asc-screenshots

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

App Store Screenshots Generator

App Store截图生成器

Overview

概述

Build a Next.js page that renders App Store screenshots for iPhone and iPad as advertisements (not UI showcases) and exports them via
html-to-image
at Apple's required resolutions. Uses FrameMe to composite app screenshots into official Apple device bezels with pixel-perfect accuracy (including Dynamic Island). Screenshots are the single most important conversion asset on the App Store.
构建一个Next.js页面,将适用于iPhone和iPad的App Store截图渲染为广告形式(而非UI展示),并通过
html-to-image
按照Apple要求的分辨率导出。使用FrameMe将应用截图合成到官方Apple设备边框中,实现像素级精准对齐(包括灵动岛)。截图是App Store上最重要的转化资产。

Core Principle

核心原则

Screenshots are advertisements, not documentation. Every screenshot sells one idea. If you're showing UI, you're doing it wrong — you're selling a feeling, an outcome, or killing a pain point.
截图是广告,不是文档。 每张截图只传递一个核心卖点。如果你只是展示UI,那你就错了——你要传递的是一种感受、一个结果,或是解决一个痛点

Step 1: Ask the User These Questions

步骤1:向用户询问以下问题

Before writing ANY code, ask the user all of these. Do not proceed until you have answers:
在编写任何代码之前,必须先向用户询问所有这些问题,得到答案后再继续:

Required

必填问题

  1. App screenshots — "Where are your app screenshots? (PNG files of actual device captures)"
  2. App icon — "Where is your app icon PNG?"
  3. Brand colors — "What are your brand colors? (accent color, text color, background preference)"
  4. Font — "What font does your app use? (or what font do you want for the screenshots?)"
  5. Feature list — "List your app's features in priority order. What's the #1 thing your app does?"
  6. Number of slides — "How many screenshots do you want? (Apple allows up to 10)"
  7. Style direction — "What style do you want? Examples: warm/organic, dark/moody, clean/minimal, bold/colorful, gradient-heavy, flat. Share App Store screenshot references if you have any."
  8. Devices — "Which devices did you capture screenshots on? (e.g. iPhone 17 Pro, iPhone 17 Pro Max, iPad Pro 11", iPad Pro 13", etc.)"
  1. 应用截图 — "你的应用截图在哪里?(实际设备捕获的PNG文件)"
  2. 应用图标 — "你的应用图标PNG文件在哪里?"
  3. 品牌色彩 — "你的品牌色彩是什么?(强调色、文字颜色、背景偏好)"
  4. 字体 — "你的应用使用什么字体?(或者你希望截图使用什么字体?)"
  5. 功能列表 — "按优先级列出你的应用功能。你的应用最核心的功能是什么?"
  6. 幻灯片数量 — "你需要多少张截图?(Apple最多允许10张)"
  7. 风格方向 — "你想要什么风格?例如:温暖/有机风、深色/氛围感、简洁/极简风、大胆/多彩风、渐变主导、扁平化。如果有参考的App Store截图,也可以分享。"
  8. 设备类型 — "你是在哪些设备上捕获的截图?(例如:iPhone 17 Pro、iPhone 17 Pro Max、iPad Pro 11英寸、iPad Pro 13英寸等)"

Optional

可选问题

  1. Localization — "Do you want screenshots in multiple languages? If so, which languages? (e.g. English, German, French). You'll get a language switcher in the preview to toggle between them."
  2. Project location — "Do you want to keep the screenshot generator project permanently so you can come back to make changes or add languages later? If so, where should I create it? (e.g. inside your app's repo)"
  3. Component assets — "Do you have any UI element PNGs (cards, widgets, etc.) you want as floating decorations? If not, that's fine — we'll skip them."
  4. Additional instructions — "Any specific requirements, constraints, or preferences?"
  1. 本地化 — "你需要多语言版本的截图吗?如果需要,是哪些语言?(例如:英语、德语、法语)。预览页面会提供语言切换器,可在不同语言间切换。"
  2. 项目位置 — "你是否希望永久保留截图生成器项目,以便后续修改或添加语言?如果是,我应该在哪里创建它?(例如:在你的应用仓库内)"
  3. 组件资源 — "你是否有想要用作浮动装饰的UI元素PNG文件(如卡片、小组件等)?如果没有也没关系,我们可以跳过这部分。"
  4. 附加说明 — "有没有任何特定的要求、限制或偏好?"

Derived from answers (do NOT ask — decide yourself)

根据答案推导(无需询问,自行决定)

Based on the user's style direction, brand colors, and app aesthetic, decide:
  • Background style: gradient direction, colors, whether light or dark base
  • Decorative elements: blobs, glows, geometric shapes, or none — match the style
  • Dark vs light slides: how many of each, which features suit dark treatment
  • Typography treatment: weight, tracking, line height — match the brand personality
  • Color palette: derive text colors, secondary colors, shadow tints from the brand colors
IMPORTANT: If the user gives additional instructions at any point during the process, follow them. User instructions always override skill defaults.
根据用户提供的风格方向、品牌色彩和应用审美,自行决定:
  • 背景风格:渐变方向、颜色、浅色或深色基底
  • 装饰元素: blob图形、光晕、几何形状或无装饰——需匹配整体风格
  • 深色/浅色幻灯片比例:各类型的数量,哪些功能适合深色风格
  • 排版处理:字重、字距、行高——需匹配品牌调性
  • 调色板:从品牌色彩中衍生文字颜色、次要颜色、阴影色调
重要提示: 如果用户在过程中提供了附加说明,请优先遵循用户的指示,用户的指示始终优先于技能默认设置。

Step 2: Set Up the Project

步骤2:搭建项目

Requirements

环境要求

  • Node.js 18+
  • FrameMe — required for pixel-perfect device framing (including Dynamic Island). Install by building from source:
    bash
    git clone https://github.com/joshluongo/frameme.git /tmp/frameme
    cd /tmp/frameme && swift build -c release
    strip .build/release/frameme
    cp .build/release/frameme /usr/local/bin/frameme
    If the user doesn't have FrameMe, ask them to install it before proceeding.
  • Node.js 18+
  • FrameMe — 用于实现像素级精准的设备边框合成(包括灵动岛)。需从源码编译安装:
    bash
    git clone https://github.com/joshluongo/frameme.git /tmp/frameme
    cd /tmp/frameme && swift build -c release
    strip .build/release/frameme
    cp .build/release/frameme /usr/local/bin/frameme
    如果用户未安装FrameMe,请让他们先完成安装再继续。

Detect Package Manager

检测包管理器

Check what's available, use this priority: bun > pnpm > yarn > npm
bash
undefined
检查可用的包管理器,优先级为:bun > pnpm > yarn > npm
bash
undefined

Check in order

按顺序检测

which bun && echo "use bun" || which pnpm && echo "use pnpm" || which yarn && echo "use yarn" || echo "use npm"
undefined
which bun && echo "use bun" || which pnpm && echo "use pnpm" || which yarn && echo "use yarn" || echo "use npm"
undefined

Scaffold (if no existing Next.js project)

初始化项目(如果没有现有Next.js项目)

bash
undefined
bash
undefined

With bun:

使用bun:

bunx create-next-app@latest . --typescript --app --src-dir --no-eslint --import-alias "@/*" bun add html-to-image jszip
bunx create-next-app@latest . --typescript --app --src-dir --no-eslint --import-alias "@/*" bun add html-to-image jszip

With pnpm:

使用pnpm:

pnpx create-next-app@latest . --typescript --app --src-dir --no-eslint --import-alias "@/*" pnpm add html-to-image jszip
pnpx create-next-app@latest . --typescript --app --src-dir --no-eslint --import-alias "@/*" pnpm add html-to-image jszip

With yarn:

使用yarn:

yarn create next-app . --typescript --app --src-dir --no-eslint --import-alias "@/*" yarn add html-to-image jszip
yarn create next-app . --typescript --app --src-dir --no-eslint --import-alias "@/*" yarn add html-to-image jszip

With npm:

使用npm:

npx create-next-app@latest . --typescript --app --src-dir --no-eslint --import-alias "@/*" npm install html-to-image jszip
undefined
npx create-next-app@latest . --typescript --app --src-dir --no-eslint --import-alias "@/*" npm install html-to-image jszip
undefined

File Structure

文件结构

project/
├── public/
│   ├── app-icon.png            # User's app icon
│   ├── framed/                 # FrameMe output (device-framed screenshots)
│   │   ├── iphone/
│   │   │   ├── en/             # Per-locale if localized UI screenshots
│   │   │   │   ├── home_framed.png
│   │   │   │   └── ...
│   │   │   └── de/             # (if single language, skip locale dirs)
│   │   │       └── ...
│   │   └── ipad/
│   │       └── ...
│   └── screenshots/            # Raw app screenshots (before framing)
│       ├── iphone/
│       │   ├── en/
│       │   │   ├── home.png
│       │   │   └── ...
│       │   └── de/
│       │       └── ...
│       └── ipad/
│           └── ...
├── src/app/
│   ├── copy.json               # Localized copy (editable without touching code)
│   ├── layout.tsx              # Font setup
│   └── page.tsx                # The screenshot generator (single file)
└── package.json
If the app UI is the same across languages (only marketing text changes), skip the locale subdirectories — all languages share the same framed device images.
The generator is
page.tsx
+
copy.json
.
No routing, no extra layouts, no API routes. All text lives in
copy.json
so the user can update copy without editing components.
project/
├── public/
│   ├── app-icon.png            # 用户的应用图标
│   ├── framed/                 # FrameMe输出(带设备边框的截图)
│   │   ├── iphone/
│   │   │   ├── en/             # 多语言版本的本地化截图
│   │   │   │   ├── home_framed.png
│   │   │   │   └── ...
│   │   │   └── de/             # (如果是单语言,可跳过语言目录)
│   │   │       └── ...
│   │   └── ipad/
│   │       └── ...
│   └── screenshots/            # 原始应用截图(未添加边框前)
│       ├── iphone/
│       │   ├── en/
│       │   │   ├── home.png
│       │   │   └── ...
│       │   └── de/
│       │       └── ...
│       └── ipad/
│           └── ...
├── src/app/
│   ├── copy.json               # 本地化文案(无需修改代码即可编辑)
│   ├── layout.tsx              # 字体设置
│   └── page.tsx                # 截图生成器(单文件实现)
└── package.json
如果应用UI在不同语言中保持一致(仅营销文案变化),则可跳过语言子目录——所有语言共享相同的带边框设备图像。
生成器核心是
page.tsx
+
copy.json
无需路由、额外布局或API路由。所有文案都存储在
copy.json
中,用户无需修改组件即可更新文案。

Font Setup

字体设置

tsx
// src/app/layout.tsx
import { YourFont } from "next/font/google"; // Use whatever font the user specified
const font = YourFont({ subsets: ["latin"] });

export default function Layout({ children }: { children: React.ReactNode }) {
  return <html><body className={font.className}>{children}</body></html>;
}
tsx
// src/app/layout.tsx
import { YourFont } from "next/font/google"; // 使用用户指定的字体
const font = YourFont({ subsets: ["latin"] });

export default function Layout({ children }: { children: React.ReactNode }) {
  return <html><body className={font.className}>{children}</body></html>;
}

Step 2.5: Frame Screenshots with FrameMe

步骤2.5:使用FrameMe添加设备边框

Before building the page, composite each raw screenshot into its device bezel using FrameMe. This produces pixel-perfect device images with the Dynamic Island, rounded corners, and bezel fully intact.
在构建页面之前,使用FrameMe将每张原始截图合成到对应的设备边框中,生成带边框的设备图像,包含灵动岛、圆角等完整元素。

Device Bezels

设备边框文件

The skill includes official Apple bezels (co-located with this SKILL.md). Use the bezel that matches the device the user captured screenshots on:
Bezel fileDeviceImage size
iphone-17-pro.png
iPhone 17 Pro (6.1")1350×2760
iphone-17-pro-max.png
iPhone 17 Pro Max (6.9")1470×3000
ipad-pro-11.png
iPad Pro 11" M41880×2640
ipad-pro-13.png
iPad Pro 13" M42300×3000
本技能包含官方Apple边框文件(与SKILL.md同目录)。请选择与用户截图捕获设备匹配的边框文件:
边框文件对应设备图像尺寸
iphone-17-pro.png
iPhone 17 Pro(6.1英寸)1350×2760
iphone-17-pro-max.png
iPhone 17 Pro Max(6.9英寸)1470×3000
ipad-pro-11.png
iPad Pro 11英寸 M41880×2640
ipad-pro-13.png
iPad Pro 13英寸 M42300×3000

Frame All Screenshots

为所有截图添加边框

Based on the user's answer to question #8, select the matching bezel:
bash
SKILL_DIR="<path to this skill's directory>"
根据用户对问题8的回答,选择匹配的边框文件:
bash
SKILL_DIR="<本技能的目录路径>"

Frame screenshots per locale (en is required, others optional)

为每种语言添加边框(英语为必填,其他语言可选)

for LANG in en de fr; do

iPhone (pick the matching bezel)

mkdir -p public/framed/iphone/$LANG frameme "$SKILL_DIR/iphone-17-pro.png" public/screenshots/iphone/$LANG/*.png --output public/framed/iphone/$LANG

OR: frameme "$SKILL_DIR/iphone-17-pro-max.png" ...

iPad (pick the matching bezel)

mkdir -p public/framed/ipad/$LANG frameme "$SKILL_DIR/ipad-pro-11.png" public/screenshots/ipad/$LANG/*.png --output public/framed/ipad/$LANG

OR: frameme "$SKILL_DIR/ipad-pro-13.png" ...

done

If the app UI doesn't change across languages (only marketing text differs), frame once under `en/` — the Device component's `onError` fallback will use English images for all locales.

FrameMe automatically:
- Detects the screen area from the bezel's alpha channel
- Creates a per-scanline mask (handles Dynamic Island, rounded corners)
- Composites the screenshot behind the bezel
- Outputs `filename_framed.png`
for LANG in en de fr; do

iPhone(选择匹配的边框)

mkdir -p public/framed/iphone/$LANG frameme "$SKILL_DIR/iphone-17-pro.png" public/screenshots/iphone/$LANG/*.png --output public/framed/iphone/$LANG

或:frameme "$SKILL_DIR/iphone-17-pro-max.png" ...

iPad(选择匹配的边框)

mkdir -p public/framed/ipad/$LANG frameme "$SKILL_DIR/ipad-pro-11.png" public/screenshots/ipad/$LANG/*.png --output public/framed/ipad/$LANG

或:frameme "$SKILL_DIR/ipad-pro-13.png" ...

done

如果应用UI在不同语言中保持一致(仅营销文案变化),只需在`en/`目录下生成一次带边框的图像——Device组件的`onError`回退机制会在其他语言图像缺失时自动使用英语版本。

FrameMe会自动完成以下操作:
- 从边框的Alpha通道检测屏幕区域
- 创建逐行扫描蒙版(处理灵动岛、圆角)
- 将截图合成到边框后方
- 输出文件名为`filename_framed.png`

Important Notes

重要注意事项

  • Match the bezel to the capture device — using a mismatched bezel will cause clipping or misalignment
  • FrameMe does NOT resize — the screenshot should match the bezel's screen area
  • The framed images are what the page.tsx will reference — the user sees the final device appearance in the ad layouts
  • 边框必须与捕获设备匹配——使用不匹配的边框会导致截图裁剪或对齐错误
  • FrameMe不支持缩放——原始截图尺寸必须与边框的屏幕区域尺寸一致
  • 带边框的图像是
    page.tsx
    将引用的文件——用户会在广告布局中看到最终的设备外观

Step 3: Plan the Slides

步骤3:规划幻灯片

Screenshot Framework (Narrative Arc)

截图框架(叙事结构)

Adapt this framework to the user's requested slide count. Not all slots are required — pick what fits:
SlotPurposeNotes
#1Hero / Main BenefitApp icon + tagline + home screen. This is the ONLY one most people see.
#2DifferentiatorWhat makes this app unique vs competitors
#3EcosystemWidgets, extensions, watch — beyond the main app. Skip if N/A.
#4+Core FeaturesOne feature per slide, most important first
2nd to lastTrust SignalIdentity/craft — "made for people who [X]"
LastMore FeaturesPills listing extras + coming soon. Skip if few features.
Rules:
  • Each slide sells ONE idea. Never two features on one slide.
  • Vary layouts across slides — never repeat the same template structure.
  • Include 1-2 contrast slides (inverted bg) for visual rhythm.
根据用户要求的幻灯片数量调整此框架,无需使用所有插槽,选择合适的即可:
插槽用途说明
#1主视觉/核心卖点应用图标 + 标语 + 主屏幕。这是大多数用户会看到的唯一一张截图。
#2差异化优势你的应用与竞品的独特之处
#3生态系统小组件、扩展、手表应用——超出主应用的附加功能。如果没有可跳过。
#4+核心功能每张幻灯片一个功能,按优先级排序
倒数第二张信任信号定位/调性——"为[某类人群]打造"
最后一张更多功能列出额外功能的胶囊式标签 + 即将推出的功能。如果功能较少可跳过。
规则:
  • 每张幻灯片只传递一个核心卖点。绝不在一张幻灯片上展示两个功能。
  • 幻灯片布局要多样化——绝不要连续使用相同的模板结构。
  • 包含1-2张对比幻灯片(反转背景),形成视觉节奏。

Step 4: Write Copy FIRST

步骤4:先编写文案

Get all headlines approved before building layouts. Bad copy ruins good design.
在构建布局之前,必须先获得所有标题的认可。糟糕的文案会毁掉优秀的设计。

The Iron Rules

铁律

  1. One idea per headline. Never join two things with "and."
  2. Short, common words. 1-2 syllables. No jargon unless it's domain-specific.
  3. 3-5 words per line. Must be readable at thumbnail size in the App Store.
  4. Line breaks are intentional. Control where lines break with
    <br />
    .
  1. 每个标题只传递一个核心思想。绝不要用"和"连接两个内容。
  2. 使用简短、常用的词汇。1-2个音节,除非是领域特定术语,否则不要使用行话。
  3. 每行3-5个单词。必须在App Store的缩略图尺寸下清晰可读。
  4. 换行是有意为之。使用
    <br />
    控制换行位置。

Three Approaches (pick one per slide)

三种文案撰写方式(每张幻灯片选择一种)

TypeWhat it doesExample
Paint a momentYou picture yourself doing it"Check your coffee without opening the app."
State an outcomeWhat your life looks like after"A home for every coffee you buy."
Kill a painName a problem and destroy it"Never waste a great bag of coffee."
类型作用示例
描绘场景让用户想象自己使用的场景"无需打开应用,即可查看咖啡状态。"
陈述结果告诉用户使用后的生活变化"为你购买的每一杯咖啡找到归宿。"
解决痛点指出问题并提供解决方案"再也不会浪费一袋好咖啡。"

What NEVER Works

绝对避免的文案类型

  • Feature lists as headlines: "Log every item with tags, categories, and notes"
  • Two ideas joined by "and": "Track X and never miss Y"
  • Compound clauses: "Save and customize X for every Y you own"
  • Vague aspirational: "Every item, tracked"
  • Marketing buzzwords: "AI-powered tips" (unless it's actually AI)
  • 以功能列表为标题:"使用标签、分类和笔记记录每一项内容"
  • 用"和"连接两个想法:"追踪X,再也不会错过Y"
  • 复合从句:"为你拥有的每一个Y保存并自定义X"
  • 模糊的励志语:"追踪每一项内容"
  • 营销 buzzwords:"AI驱动的建议"(除非真的使用了AI)

Copy Process

文案流程

  1. Write 3 options per slide using the three approaches
  2. Read each at arm's length — if you can't parse it in 1 second, it's too complex
  3. Check: does each line have 3-5 words? If not, adjust line breaks
  4. Present options to the user with reasoning for each
  5. If localized: translate approved headlines into all requested languages. Respect the tone/formality of each language. Line breaks may need adjusting per language — German and French text is often longer than English.
  1. 为每张幻灯片使用三种方式各写3个备选文案
  2. 把文案放在一臂距离外阅读——如果1秒内无法理解,说明太复杂
  3. 检查:每行是否有3-5个单词?如果没有,调整换行
  4. 将备选文案及理由呈现给用户
  5. 如果需要本地化:将认可的标题翻译成所有要求的语言。尊重每种语言的语气/正式程度。不同语言可能需要调整换行——德语和法语的文本通常比英语长。

Reference Apps for Copy Style

文案风格参考应用

  • Raycast — specific, descriptive, one concrete value per slide
  • Turf — ultra-simple action verbs, conversational
  • Mela / Notion — warm, minimal, elegant
  • Raycast — 具体、描述性强,每张幻灯片传递一个明确的价值
  • Turf — 超简洁的动作动词,口语化风格
  • Mela / Notion — 温暖、极简、优雅

Step 5: Build the Page

步骤5:构建页面

Architecture

架构

page.tsx
├── Constants (IPHONE_W, IPHONE_H, IPAD_W, IPAD_H, design tokens)
├── LOCALES (code, label, asc locale code for App Store Connect)
├── ASC_DEVICE (maps "iphone" → "APP_IPHONE_67", "ipad" → "APP_IPAD_PRO_3GEN_129")
├── COPY (imported from copy.json — localized strings per language, keyed by slide name)
├── Device component (renders a pre-framed device image — just an <img>)
├── Caption component (label + headline)
├── Decorative components (blobs, glows, shapes — based on style direction)
├── Screenshot1..N components (one per slide, with iPhone and/or iPad variants)
├── SCREENSHOTS array (registry, with device type tag)
├── ScreenshotPreview (ResizeObserver scaling + hover export)
└── ScreenshotsPage (grid + toolbar with language switcher + device filter + Export/Export All Languages)
page.tsx
├── 常量(IPHONE_W, IPHONE_H, IPAD_W, IPAD_H, 设计令牌)
├── LOCALES(代码、标签、用于App Store Connect的ASC语言代码)
├── ASC_DEVICE(映射关系:"iphone" → "APP_IPHONE_67", "ipad" → "APP_IPAD_PRO_3GEN_129")
├── COPY(从copy.json导入——每种语言的本地化字符串,按幻灯片名称索引)
├── Device组件(渲染带边框的设备图像——仅为<img>标签)
├── Caption组件(标签 + 标题)
├── 装饰组件(blob图形、光晕、形状——基于风格方向)
├── Screenshot1..N组件(每张幻灯片一个组件,包含iPhone和/或iPad变体)
├── SCREENSHOTS数组(注册表,带设备类型标签)
├── ScreenshotPreview(ResizeObserver缩放 + 悬停导出)
└── ScreenshotsPage(网格 + 工具栏,包含语言切换器 + 设备过滤器 + 导出/导出所有语言)

Export Sizes (Largest Only — Apple auto-scales to smaller displays)

导出尺寸(仅需最大尺寸——Apple会自动缩放到较小屏幕)

App Store Connect accepts the largest screenshot size per device and automatically scales it for smaller displays. Only generate:
typescript
// iPhone 6.9" — covers all iPhone sizes
const IPHONE_W = 1320;
const IPHONE_H = 2868;

// iPad 13" — covers all iPad sizes
const IPAD_W = 2064;
const IPAD_H = 2752;
App Store Connect接受每种设备的最大截图尺寸,并自动缩放到较小屏幕。只需生成以下尺寸:
typescript
// iPhone 6.9英寸 — 覆盖所有iPhone尺寸
const IPHONE_W = 1320;
const IPHONE_H = 2868;

// iPad 13英寸 — 覆盖所有iPad尺寸
const IPAD_W = 2064;
const IPAD_H = 2752;

Rendering Strategy

渲染策略

Each screenshot is designed at full export resolution. Two copies exist:
  1. Preview: CSS
    transform: scale()
    via ResizeObserver to fit a grid card
  2. Export: Offscreen at
    position: absolute; left: -9999px
    at true resolution
If both iPhone and iPad are requested:
  • Separate sections: Render iPhone and iPad in distinct sections with device headers (
    <h2>iPhone</h2>
    ,
    <h2>iPad</h2>
    ), each with its own grid. Do NOT mix them in a single grid.
  • Device filter: The toolbar includes an All / iPhone / iPad toggle that shows/hides sections.
  • Each section filters
    SCREENSHOTS.filter(s => s.device === "iphone")
    etc.
每张截图都以完整导出分辨率设计,存在两个版本:
  1. 预览版:通过ResizeObserver使用CSS
    transform: scale()
    适配网格卡片
  2. 导出版:通过
    position: absolute; left: -9999px
    在屏幕外以真实分辨率渲染
如果同时需要iPhone和iPad版本:
  • 分开的区域:将iPhone和iPad截图渲染在不同的区域,分别添加设备标题(
    <h2>iPhone</h2>
    <h2>iPad</h2>
    ),每个区域有自己的网格。不要将它们混合在同一个网格中。
  • 设备过滤器:工具栏包含全部/iPhone/iPad的切换按钮,可显示/隐藏对应区域。
  • 每个区域通过
    SCREENSHOTS.filter(s => s.device === "iphone")
    等方式过滤截图。

Localization

本地化

If the user wants multiple languages, store all copy in a separate
copy.json
file so the user can edit text without touching code:
src/app/copy.json
json
{
  "en": {
    "iphone-hero": { "headline": "Spam texts,<br/>silenced.", "pills": ["Smart", "Private"] },
    "iphone-privacy": { "label": "On-device AI", "headline": "Completely<br/>private.", "sub": "..." }
  },
  "de": {
    "iphone-hero": { "headline": "Spam-SMS,<br/>verstummt.", "pills": ["Intelligent", "Datenschutz"] },
    "iphone-privacy": { "label": "KI auf dem Gerät", "headline": "Vollständig<br/>privat.", "sub": "..." }
  }
}
Import and type-cast it in
page.tsx
:
typescript
import COPY_JSON from "./copy.json";
const COPY = COPY_JSON as Record<Locale, Record<string, SlideCopy>>;
Each slide component receives the current locale and looks up its copy from
COPY[locale][slideName]
. The toolbar includes a language switcher (
<select>
dropdown) that updates a shared
locale
state. Options show
{label} ({code})
(e.g. "English (en)") and are sorted alphabetically by label. This lets the user preview all slides in any language before exporting.
When exporting, include the locale in filenames:
01-iphone-hero-en-1320x2868.png
,
01-iphone-hero-de-1320x2868.png
. The "Export All" button exports the currently selected language. If the user wants all languages, they switch and export each.
Localized device images: If app screenshots differ per language (e.g. localized UI), organize framed images by locale:
public/framed/iphone/en/
,
public/framed/iphone/de/
, etc. Slide components use the current locale in the path:
src={
/framed/iphone/${locale}/iPhone-01_framed.png
}
. English (
en
) is the required default — all other languages are optional and fall back to English if the image is missing (via
onError
on the Device component).
如果用户需要多语言版本,将所有文案存储在单独的
copy.json
文件中,用户无需修改代码即可编辑文本:
src/app/copy.json
json
{
  "en": {
    "iphone-hero": { "headline": "Spam texts,<br/>silenced.", "pills": ["Smart", "Private"] },
    "iphone-privacy": { "label": "On-device AI", "headline": "Completely<br/>private.", "sub": "..." }
  },
  "de": {
    "iphone-hero": { "headline": "Spam-SMS,<br/>verstummt.", "pills": ["Intelligent", "Datenschutz"] },
    "iphone-privacy": { "label": "KI auf dem Gerät", "headline": "Vollständig<br/>privat.", "sub": "..." }
  }
}
page.tsx
中导入并进行类型转换:
typescript
import COPY_JSON from "./copy.json";
const COPY = COPY_JSON as Record<Locale, Record<string, SlideCopy>>;
每个幻灯片组件接收当前语言参数,并从
COPY[locale][slideName]
中查找对应的文案。工具栏包含一个语言切换器
<select>
下拉菜单),用于更新共享的
locale
状态。选项显示
{label} ({code})
(例如:"English (en)"),并按标签字母顺序排序。用户可在导出前预览所有语言的幻灯片。
导出时,文件名中需包含语言信息:
01-iphone-hero-en-1320x2868.png
01-iphone-hero-de-1320x2868.png
。"导出所有语言"按钮会导出当前选中的语言。如果用户需要所有语言版本,需切换到对应语言后分别导出。
本地化设备图像:如果应用截图因语言不同而变化(例如:本地化UI),需按语言组织带边框的图像:
public/framed/iphone/en/
public/framed/iphone/de/
等。幻灯片组件在路径中使用当前语言:
src={
/framed/iphone/${locale}/iPhone-01_framed.png
}
。英语(
en
)是必填的默认语言——如果其他语言的图像缺失,Device组件的
onError
回退机制会自动使用英语版本。

Device Component

Device组件

Since FrameMe produces complete device-framed images (bezel + screenshot composited together), the device component is just an
<img>
tag — no CSS clipping, no screen offset calculations, no border-radius hacks:
tsx
function Device({ src, alt, style }: {
  src: string; alt: string; style?: React.CSSProperties;
}) {
  return (
    <img
      src={src}
      alt={alt}
      style={style}
      draggable={false}
      onError={(e) => {
        // Fall back to English if localized image is missing
        const img = e.currentTarget;
        const enSrc = img.src.replace(/\/(?!en\/)[a-z]{2}\//, "/en/");
        if (img.src !== enSrc) img.src = enSrc;
      }}
    />
  );
}
The framed images already contain the device bezel with the Dynamic Island, rounded corners, and screen content composited pixel-perfectly. The transparent areas around the device allow it to sit naturally on any background. The
onError
fallback ensures missing locale-specific images gracefully degrade to English.
由于FrameMe生成的是完整的带边框设备图像(边框与截图已合成),Device组件仅需一个
<img>
标签——无需CSS裁剪、屏幕偏移计算或圆角技巧:
tsx
function Device({ src, alt, style }: {
  src: string; alt: string; style?: React.CSSProperties;
}) {
  return (
    <img
      src={src}
      alt={alt}
      style={style}
      draggable={false}
      onError={(e) => {
        // 如果本地化图像缺失,回退到英语版本
        const img = e.currentTarget;
        const enSrc = img.src.replace(/\/(?!en\/)[a-z]{2}\//, "/en/");
        if (img.src !== enSrc) img.src = enSrc;
      }}
    />
  );
}
带边框的图像已包含带灵动岛、圆角的设备边框,且与截图像素级对齐。设备周围的透明区域使其能自然地放置在任何背景上。
onError
回退机制确保缺失的本地化图像会优雅地降级为英语版本。

Typography (Resolution-Independent)

排版(与分辨率无关)

All sizing relative to canvas width W (use
IPHONE_W
or
IPAD_W
depending on device):
ElementSizeWeightLine Height
Category label
W * 0.028
600 (semibold)default
Headline
W * 0.09
to
W * 0.1
700 (bold)1.0
Hero headline
W * 0.1
700 (bold)0.92
所有尺寸相对于画布宽度W(根据设备类型使用
IPHONE_W
IPAD_W
):
元素尺寸字重行高
分类标签
W * 0.028
600(半粗体)默认
标题
W * 0.09
W * 0.1
700(粗体)1.0
主视觉标题
W * 0.1
700(粗体)0.92

Device Placement Patterns

设备放置模式

Vary across slides — NEVER use the same layout twice in a row:
Centered device (hero, single-feature):
bottom: 0, width: "82-86%", translateX(-50%) translateY(12-14%)
Two devices layered (comparison):
Back: left: "-8%", width: "65%", rotate(-4deg), opacity: 0.55
Front: right: "-4%", width: "82%", translateY(10%)
Device + floating elements (only if user provided component PNGs):
Cards should NOT block the device's main content.
Position at edges, slight rotation (2-5deg), drop shadows.
If distracting, push partially off-screen or make smaller.
幻灯片布局要多样化——绝不要连续使用相同的布局
居中设备(主视觉、单一功能):
bottom: 0, width: "82-86%", translateX(-50%) translateY(12-14%)
双设备层叠(对比):
后方设备: left: "-8%", width: "65%", rotate(-4deg), opacity: 0.55
前方设备: right: "-4%", width: "82%", translateY(10%)
设备 + 浮动元素(仅当用户提供了组件PNG文件时):
卡片不应遮挡设备的主要内容。
放置在边缘,轻微旋转(2-5度),添加阴影。
如果分散注意力,可部分移出屏幕或缩小尺寸。

iPad-Specific Layout Notes

iPad专属布局注意事项

iPad screenshots have a landscape-ish aspect ratio (2064×2752, closer to 3:4 vs iPhone's ~1:2.2). This affects layouts:
  • The device image is wider relative to the canvas — use narrower widths (70-78% instead of 82-86%) to avoid crowding
  • Captions may need to sit above the device rather than beside it, since the wider device leaves less horizontal space
  • Two-device layouts work best with smaller scaling and more overlap
  • iPad's larger visible screen area means the app UI is more readable — lean into showing the content
iPad截图的宽高比接近横屏(2064×2752,接近3:4,而iPhone约为1:2.2),这会影响布局:
  • 设备图像相对于画布更宽——需使用更窄的宽度(70-78%,而非82-86%),避免拥挤
  • 文案可能需要放置在设备上方,而非旁边,因为更宽的设备会占用更多水平空间
  • 双设备布局最适合更小的缩放比例和更多的重叠
  • iPad更大的可见屏幕区域意味着应用UI更易读——可更突出地展示内容

"More Features" Slide (Optional)

"更多功能"幻灯片(可选)

Dark/contrast background with app icon, headline ("And so much more."), and feature pills. Can include a "Coming Soon" section with dimmer pills.
深色/对比背景,包含应用图标、标题("还有更多功能。")和功能胶囊标签。可包含"即将推出"的区域,使用更暗的胶囊标签。

Step 6: Export

步骤6:导出

Why html-to-image, NOT html2canvas

为什么使用html-to-image,而非html2canvas

html2canvas
breaks on CSS filters, gradients, drop-shadow, backdrop-filter, and complex clipping.
html-to-image
uses native browser SVG serialization — handles all CSS faithfully.
html2canvas
在处理CSS滤镜、渐变、drop-shadow、backdrop-filter和复杂裁剪时会出现问题。
html-to-image
使用原生浏览器SVG序列化——能忠实地处理所有CSS属性。

Export Implementation

导出实现

typescript
import { toPng } from "html-to-image";

// CRITICAL: el must be the slide component's root div (position: relative),
// NOT the offscreen wrapper. Use ref on wrapper, then ref.firstElementChild.
// toPng must capture the element that owns the positioned children.
const wrapper = el.parentElement!;

// Before capture: move wrapper on-screen
wrapper.style.left = "0px";
wrapper.style.opacity = "1";
wrapper.style.zIndex = "-1";

const opts = { width: W, height: H, pixelRatio: 1, cacheBust: true };

// CRITICAL: Double-call trick — first warms up fonts/images, second produces clean output
await toPng(el, opts);
const dataUrl = await toPng(el, opts);

// After capture: move wrapper back off-screen
wrapper.style.left = "-9999px";
wrapper.style.opacity = "";
wrapper.style.zIndex = "";
Use
W = IPHONE_W, H = IPHONE_H
for iPhone slides and
W = IPAD_W, H = IPAD_H
for iPad slides.
Offscreen container setup: The wrapper div is
position: absolute; left: -9999px
with
fontFamily
set. The ref should resolve to the slide's root div inside it (via
firstElementChild
), not the wrapper itself:
tsx
ref={(el) => { if (el?.firstElementChild) exportRefs.current.set(i, el.firstElementChild as HTMLDivElement); }}
typescript
import { toPng } from "html-to-image";

// 关键:el必须是幻灯片组件的根div(position: relative),
// 而非屏幕外的包装器。在包装器上使用ref,然后取ref.firstElementChild。
// toPng必须捕获拥有定位子元素的元素。
const wrapper = el.parentElement!;

// 捕获前:将包装器移到屏幕内
wrapper.style.left = "0px";
wrapper.style.opacity = "1";
wrapper.style.zIndex = "-1";

const opts = { width: W, height: H, pixelRatio: 1, cacheBust: true };

// 关键:双重调用技巧——第一次调用预加载字体/图像,第二次生成清晰的输出
await toPng(el, opts);
const dataUrl = await toPng(el, opts);

// 捕获后:将包装器移回屏幕外
wrapper.style.left = "-9999px";
wrapper.style.opacity = "";
wrapper.style.zIndex = "";
iPhone幻灯片使用
W = IPHONE_W, H = IPHONE_H
,iPad幻灯片使用
W = IPAD_W, H = IPAD_H
屏幕外容器设置:包装器div使用
position: absolute; left: -9999px
,并设置
fontFamily
。ref应指向其中的幻灯片根div(通过
firstElementChild
),而非包装器本身:
tsx
ref={(el) => { if (el?.firstElementChild) exportRefs.current.set(i, el.firstElementChild as HTMLDivElement); }}

Key Rules

关键规则

  • Double-call trick: First
    toPng()
    loads fonts/images lazily. Second produces clean output. Without this, exports are blank.
  • On-screen for capture: Temporarily move to
    left: 0
    before calling
    toPng
    .
  • Offscreen wrapper: Use
    position: absolute; left: -9999px
    (not
    fixed
    ). Do NOT set
    width
    /
    height
    on the wrapper — let the slide component define its own dimensions.
  • No resizing needed: Each slide is already at the exact export resolution. No canvas scaling step.
  • Zip export (asc-client compatible): Two export buttons. "Export ({LOCALE})" exports the current language. "Export All Languages" loops through every locale (sets state, waits for re-render, captures). Both create a zip compatible with asc-client. Structure:
    en-US/APP_IPHONE_67/01_hero.png
    en-US/APP_IPAD_PRO_3GEN_129/01_hero.png
    de-DE/APP_IPHONE_67/01_hero.png
    Device type mapping:
    iphone → APP_IPHONE_67
    ,
    ipad → APP_IPAD_PRO_3GEN_129
    . Each locale entry needs an
    asc
    field for the App Store Connect locale code (e.g.
    { code: "en", label: "English", asc: "en-US" }
    ). Files are ordered alphabetically so use zero-padded prefixes:
    01_name.png
    .
  • Single-click export: Downloads individual PNGs with device in filename:
    01-iphone-hero-en-1320x2868.png
    .
  • 300ms delay between sequential exports.
  • Set
    fontFamily
    on the offscreen container.
  • Use
    String(index + 1).padStart(2, "0")
    for numbering.
  • 双重调用技巧:第一次
    toPng()
    预加载字体和图像,第二次生成清晰的输出。如果不使用此技巧,导出内容可能为空。
  • 捕获前移到屏幕内:在调用
    toPng
    前将元素移到屏幕内。
  • 屏幕外包装器:使用
    position: absolute; left: -9999px
    (不要使用
    fixed
    )。不要在包装器上设置
    width
    /
    height
    ——让幻灯片组件自己定义尺寸。
  • 无需缩放:每张幻灯片已按精确的导出分辨率设计,无需额外的画布缩放步骤。
  • 兼容asc-client的Zip导出:提供两个导出按钮。"导出({语言})"导出当前语言的截图。"导出所有语言"循环遍历所有语言(更新状态,等待重新渲染,捕获截图)。两者都生成兼容asc-client的Zip文件,结构如下:
    en-US/APP_IPHONE_67/01_hero.png
    en-US/APP_IPAD_PRO_3GEN_129/01_hero.png
    de-DE/APP_IPHONE_67/01_hero.png
    设备类型映射:
    iphone → APP_IPHONE_67
    ipad → APP_IPAD_PRO_3GEN_129
    。每种语言条目需要一个
    asc
    字段,对应App Store Connect的语言代码(例如:
    { code: "en", label: "English", asc: "en-US" }
    )。文件按字母顺序排序,因此使用零填充前缀:
    01_name.png
  • 一键导出:下载单个PNG文件,文件名包含设备信息:
    01-iphone-hero-en-1320x2868.png
  • 连续导出之间需有300ms的延迟。
  • 在屏幕外容器上设置
    fontFamily
  • 使用
    String(index + 1).padStart(2, "0")
    生成编号。

Common Mistakes

常见错误

MistakeFix
All slides look the sameVary device position (center, left, right, two-device, no-device)
Decorative elements invisibleIncrease size and opacity — better too visible than invisible
Copy is too complex"One second at arm's length" test
Floating elements block the deviceMove off-screen edges or above the device
Plain white/black backgroundUse gradients — even subtle ones add depth
Too clutteredRemove floating elements, simplify to device + caption
Too simple/emptyAdd larger decorative elements, floating items at edges
Headlines use "and"Split into two slides or pick one idea
No visual contrast across slidesMix light and dark backgrounds
Export is blankUse double-call trick; move element on-screen before capture
Export has empty top, content at bottom
toPng
must target the slide's root div (has
position: relative
), not the offscreen wrapper
FrameMe not installedBuild from source: clone repo,
swift build -c release
, copy binary to PATH
Framed image looks wrongEnsure raw screenshot matches the bezel's screen resolution
错误修复方法
所有幻灯片看起来都一样改变设备位置(居中、左对齐、右对齐、双设备、无设备)
装饰元素不可见增大尺寸和透明度——宁可过于明显,也不要看不见
文案过于复杂进行"一臂距离1秒阅读"测试
浮动元素遮挡设备移到屏幕边缘或设备上方
纯白/纯黑背景使用渐变——即使是细微的渐变也能增加深度
过于拥挤移除浮动元素,简化为设备 + 文案
过于简单/空白添加更大的装饰元素,在边缘添加浮动元素
标题使用"和"拆分为两张幻灯片或选择一个核心思想
幻灯片之间无视觉对比混合使用浅色和深色背景
导出内容为空使用双重调用技巧;捕获前将元素移到屏幕内
导出内容顶部空白,内容在底部
toPng
必须指向幻灯片的根div(拥有
position: relative
),而非屏幕外的包装器
未安装FrameMe从源码编译:克隆仓库,执行
swift build -c release
,将二进制文件复制到PATH
带边框的图像显示异常确保原始截图与边框的屏幕分辨率匹配