當需要建置 App Store 或 Google Play 截圖頁面、產生可匯出的 iOS 及/或 Android 應用程式宣傳截圖,或是使用 Next.js 快速搭建截圖編輯器時使用。觸發詞包含:app store、play store、screenshots(截圖)、marketing assets(行銷素材)、html-to-image、phone mockup(手機樣機)、android screenshots、feature graphic(宣傳圖)。
App Store & Google Play 截圖產生器
總覽
快速搭建預先建置好的 Next.js + ShadCN 編輯器,讓使用者能夠將 App Store 與 Google Play 的截圖設計並匯出為廣告宣傳圖(非單純的 UI 展示圖)。該編輯器包辦了所有繁重的工作:
- 按照畫布真實解析度的即時連動預覽(縮放至合適大小)
- 拖曳重新排序畫面、單元內文字編輯、各畫面獨立的版面切換器
- 跨畫面 Mockup:手機/裝置外框、標題文案與分層元素可跨越相鄰畫面移動,並匯出為裁切後的成果
- 支援拖放上傳的截圖選擇器(檔案 → 儲存至
public/screenshots/uploaded/<hash>.png) - 自動儲存至專案根目錄的
app-store-screenshots.json(可納入 git 版本控制)+localStorage鏡像同步 - 輕鬆切換 iOS ↔ Android 平台 — 分開的卡片集(Slide Decks)可並排運作
- 透過
html-to-image一鍵批次匯出符合 Apple/Google 所有指定解析度的 PNG 檔 - 單張投影片可切換淺色/深色模式、主題預設集、語系選擇
- 針對此 Skill 建立的舊版專案提供引導式原地轉移(Migration);被動與顯式轉移會保持舊版卡片集隔離,直到使用者主動選擇啟用連動畫布(Connected Canvas)
開箱即用的支援裝置:
- iPhone(直向)— Apple App Store
- iPad(直向)— Apple App Store
- Android 手機(直向)— Google Play
- Android 7 吋平板(直向 + 橫向)— Google Play
- Android 10 吋平板(直向 + 橫向)— Google Play
- Feature Graphic(1024×500 橫幅)— Google Play 應用程式頁面頂部宣傳圖
核心原則
截圖是廣告宣傳,不是說明文件。 每張截圖都只賣一個核心理念。如果你只是展示 UI,那就做錯了 — 你賣的是一種感覺、一種成果,或是擊中使用者痛點的解方。請善用此 Skill 的互動式編輯器來快速迭代文案與排版;切勿從零開始手動打造頁面。
此 Skill 的工作流程
- 複製預先建置好的範本:從
template/(與此SKILL.md位於同目錄)複製檔案至使用者的工作目錄。 - 使用使用者的套件管理器安裝相依套件。
- 將使用者的截圖放進
public/screenshots/...,並將 App 圖示放進public/。 - (可選)預先填充
app-store-screenshots.json,寫入使用者的 App 名稱、初始文案、截圖與連動畫布偏好設定,讓首次預覽就能看到實際效果。 - 啟動開發伺服器(dev server),並提示使用者在瀏覽器中開啟編輯器。
你不應該手寫 page.tsx、裝置外框(device frames)或匯出邏輯。這些都已經包含在範本中了。
步驟 0:偵測現有的截圖專案
在步驟 1 詢問新建專案的問題之前,請務必先檢查當前工作目錄中是否已存在 app-store-screenshots 的實作。
執行輕量化檢查命令:
test -f package.json && sed -n '1,220p' package.json
test -f app-store-screenshots.json && sed -n '1,120p' app-store-screenshots.json
rg -n "app-store-screenshots|html-to-image|toPng|ScreenshotEditor|DeckCanvas|connectedCanvas|EXPORT_SIZES|mockup.png|PHONE_SCREEN" package.json src app public 2>/dev/null
find public -maxdepth 4 \( -path "*/screenshots*" -o -name "mockup.png" -o -name "app-icon.png" \) -print 2>/dev/null
當滿足以下任一條件時,請將該專案視為舊版實作:
- 存在
app-store-screenshots.json但缺少schemaVersion、schemaVersion < 2,或缺少connectedCanvas欄位。 - 存在
src/components/editor/screenshot-editor.tsx但編輯器中未引用DeckCanvas或connectedCanvas。 src/app/page.tsx包含舊版的全功能單一檔案產生器(如html-to-image、toPng、EXPORT_SIZES、PHONE_SCREEN、硬編碼的投影片陣列/主題)。- 專案庫中包含舊版的截圖資產佈局(
public/mockup.png、public/screenshots...)以及截圖產生器套件設定。
如果偵測到舊版實作,在進行任何其他操作前,請只詢問一個問題:
在此處偵測到舊版的 App Store 截圖專案。請問是否要將現有專案轉移(Migrate)至新的連動畫布(connected-canvas)編輯器?
- 是 — 將現有專案轉移至新版編輯器
- 否 — 以其他方式設定或修改專案
如果使用者選擇 是,請不要詢問步驟 1 的問卷。請使用專案庫中現有的檔案執行下方的轉移流程。如果使用者選擇 否,請繼續進行步驟 1。
轉移流程(當使用者選擇「是」時)
目標是進行原地 UI/範本升級,而不是重新設計。只要現有檔案中已存在使用者的 App 名稱、文案、截圖路徑、App 圖示、已上傳資產、語系和裝置卡片集,就必須完整保留。用當前範本替換舊的 UI 實作。除非專案已經明確啟用連動畫布,否則請保持舊版卡片集於獨立匯出模式。
轉移規則:
- 切勿詢問更多產品/設計問題。 使用者已經有現成專案。請從現有檔案中進行推斷,並在最後報告任何不影響運作的缺失資訊。
- 絕不刪除使用者資產。 務必保留
public/screenshots/、public/app-icon.png、已上傳的截圖以及任何現有的app-store-screenshots.json。 - 保持可復原性。 若工作區(worktree)不乾淨,請勿復原無關的變更。在覆寫範本檔案前,將被替換的專案狀態/資產/程式碼快照複製到專案外的臨時備份目錄(例如
/tmp/app-store-screenshots-migration-<timestamp>/),並在最終回覆中註明該路徑。 - 優先使用結構化轉移。 請使用 JSON 工具讀取與寫入
app-store-screenshots.json。請勿使用 Regex 編輯 JSON。 - 設定
schemaVersion: 2並維護舊版connectedCanvas的安全性。 如果現有專案已有明確的布林值connectedCanvas,請予以保留。如果專案是 v2 之前的版本或缺少此標記,請寫入"connectedCanvas": false,以防螢幕外/裁切的舊版 Mockup 溢出到相鄰的匯出圖中。新專案預設仍為連動畫布。 - 保持截圖路徑指向現有檔案。 除非舊專案原本就依賴數字命名且轉移過程需要它們,否則請勿重新命名截圖檔案。保留現有的靜態路徑即可。
- 自動處理自訂主題(無須詢問)。 如果舊專案引用了自訂的
themeId,且能在舊檔案中找到對應的主題物件,請將該主題物件合併到全新的src/lib/constants.ts中。如果無法復原,請在專案 JSON 中保留該themeId;編輯器會自動降級回clean-light並發出警告,同時你應提醒使用者需要手動復原自訂主題。 - 盡可能合併 package 元數據。 截圖編輯器必須以範本的 dependencies 與 scripts 為準,但應保留無衝突的現有
dependencies、devDependencies和實用指令,除非直接發生衝突。 - 請勿將範本範例卡片集匯入實際轉移中。 如果舊專案已有卡片集或截圖,請僅將範本用於 UI/程式碼。轉移後的專案中不要包含範本的範例截圖/卡片集,以免使用者的 App 繼承無關的範例內容。
- 使用一次性副本進行 Dogfooding 測試。 如果使用者要求測試或審查轉移過程而非直接轉移專案,請將 App 複製到臨時目錄或工作區並在該處執行轉移。只有在使用明確要求實際轉移並回答 是 時,才可動用真實的程式碼庫。
推薦的轉移步驟命令序列:
# 1. 將有用的舊檔案快照備份到專案外部。
STAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="/tmp/app-store-screenshots-migration-$STAMP"
mkdir -p "$BACKUP_DIR"
cp -R app-store-screenshots.json public src package.json tailwind.config.ts next.config.mjs "$BACKUP_DIR/" 2>/dev/null || true
# 2. 保留必須在複製範本後存活的專案狀態與資產。
PRESERVE_DIR="$BACKUP_DIR/preserve"
mkdir -p "$PRESERVE_DIR"
cp app-store-screenshots.json "$PRESERVE_DIR/" 2>/dev/null || true
cp -R public/screenshots "$PRESERVE_DIR/screenshots" 2>/dev/null || true
cp public/app-icon.png "$PRESERVE_DIR/app-icon.png" 2>/dev/null || true
# 3. 將當前範本複製並覆蓋舊的 UI 實作。
cp -R "<SKILL_DIR>/template/." "$PWD/"
cp app-store-screenshots.json "$BACKUP_DIR/template-app-store-screenshots.json" 2>/dev/null || true
# 4. 將保留的使用者狀態/資產還原回範本範例之上。
cp "$PRESERVE_DIR/app-store-screenshots.json" app-store-screenshots.json 2>/dev/null || true
mkdir -p public
if [ -d "$PRESERVE_DIR/screenshots" ]; then
mkdir -p "$BACKUP_DIR/template-samples/public"
mv public/screenshots "$BACKUP_DIR/template-samples/public/screenshots" 2>/dev/null || true
cp -R "$PRESERVE_DIR/screenshots" public/screenshots
else
mkdir -p public/screenshots
fi
cp "$PRESERVE_DIR/app-icon.png" public/app-icon.png 2>/dev/null || true
在複製完成後,請升級或建立 app-store-screenshots.json。若已存在專案檔案,請原地修正(coerce)。若不存在專案檔案但舊的投影片資料嵌入在 src/lib/defaults.ts 或 src/app/page.tsx 中,請盡力將其擷取至範本的專案 JSON 中,若失敗再降級使用預設投影片。請優先尋找名為 slides、screens、features、defaultSlides、appName、tagline、theme 和截圖路徑的舊陣列或物件。若舊實作中僅有圖片檔案,請依路徑排序 public/screenshots/** 並以此生成初始投影片。
請使用如下微型 JSON 指令碼進行最終的專案狀態強制轉換:
BACKUP_DIR="$BACKUP_DIR" node <<'NODE'
const fs = require("fs");
const path = require("path");
const PROJECT_FILE = "app-store-screenshots.json";
const DEFAULT_LOCALE = "en";
const DEVICE_KEYS = ["iphone", "ipad", "android", "android-7", "android-10", "feature-graphic"];
const LAYOUTS = ["hero", "device-bottom", "device-top", "two-devices", "no-device", "split-landscape", "feature-graphic"];
function readJson(file) {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return null;
}
}
const templateState =
readJson(path.join(process.env.BACKUP_DIR || "", "template-app-store-screenshots.json")) ||
readJson(PROJECT_FILE) ||
{};
const existingState = readJson(PROJECT_FILE) || {};
const hasExplicitConnectedCanvas = typeof existingState.connectedCanvas === "boolean";
const existingDecks =
existingState.slidesByDevice && typeof existingState.slidesByDevice === "object"
? existingState.slidesByDevice
: {};
const hasExistingDecks = Object.keys(existingDecks).length > 0;
const state = {
...templateState,
...existingState,
slidesByDevice: hasExistingDecks ? existingDecks : templateState.slidesByDevice || {},
};
const legacySlides =
Array.isArray(existingState.slides) ? existingState.slides :
Array.isArray(existingState.screens) ? existingState.screens :
Array.isArray(existingState.features) ? existingState.features :
null;
if (legacySlides && !hasExistingDecks) {
state.slidesByDevice = {
iphone: legacySlides,
};
}
function localized(value) {
if (typeof value === "string") return { [DEFAULT_LOCALE]: value };
if (value && typeof value === "object") return value;
return {};
}
function cleanTransform(value) {
if (!value || typeof value !== "object") return undefined;
const { x, y, width, height, rotation, zIndex } = value;
if (![x, y, width, height].every((n) => typeof n === "number" && Number.isFinite(n))) return undefined;
return {
x,
y,
width: Math.max(1, width),
height: Math.max(1, height),
...(typeof rotation === "number" && Number.isFinite(rotation) ? { rotation } : {}),
...(typeof zIndex === "number" && Number.isFinite(zIndex) ? { zIndex } : {}),
};
}
function firstString(...values) {
return values.find((value) => typ






