176 lines
17 KiB
Markdown
176 lines
17 KiB
Markdown
---
|
|
name: store-screenshots
|
|
description: Produce store screenshots and the Play feature graphic for a mobile app or game in every required size and many languages - capture raw frames from the running app or game (web build via playwright, iOS Simulator via simctl, Android emulator or device via adb, or a folder of frames from any engine), write per-language marketing copy (incl. RTL Arabic and CJK fonts), render captioned scenes with device frames, review them in a local gallery, A/B variants for Play store listing experiments and App Store Product Page Optimization tests (created and filled through the API), JPEG copies for Play, and upload to both stores. Use when the user needs App Store / Google Play screenshots, preview images, a feature graphic, localized screenshot captions, or screenshot sizes.
|
|
---
|
|
|
|
# Store screenshots
|
|
|
|
CLI: `storebound` (or `bunx storebound` in a project that has it, or `bun <toolchain>/cli/index.ts`). Needs `ffmpeg`, and `playwright` in the project
|
|
(`bun add -d playwright`; it uses Playwright's Chromium or an installed Google Chrome) for rendering and web capture.
|
|
iOS Simulator capture needs macOS + Xcode (`xcrun`); Android capture needs `adb` (platform-tools).
|
|
|
|
## Sizes (what each store wants)
|
|
| size key | pixels | where |
|
|
|---|---|---|
|
|
| ios69 | 1320 x 2868 | App Store 6.9" (`APP_IPHONE_67`, "Dynamic Island (large display)"); the set storebound requires |
|
|
| ios65 | 1284 x 2778 | App Store 6.5" (`APP_IPHONE_65`) |
|
|
| ipad13 | 2064 x 2752 | App Store 13" iPad (`APP_IPAD_PRO_3GEN_129`), required when the app runs on iPad; only with `apple.ipad: true` |
|
|
| play | 1080 x 1920 | Play phone (2-8, ratio at most 2:1) |
|
|
| tab7 | 1080 x 1920 | Play 7" tablet (tablet frames, Android tablet body); optional, see `play.tablets` |
|
|
| tab10 | 1440 x 2560 | Play 10" tablet; optional, see `play.tablets` |
|
|
| feature | 1024 x 500 | Play feature graphic (required) |
|
|
|
|
Sources (re-check before a release; Apple renames display classes often):
|
|
- Apple "Screenshot specifications": https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications
|
|
(13" iPad: 2064 x 2752 or 2048 x 2732, landscape too, "Required if app runs on iPad"; 1-10 per size; JPEG / PNG,
|
|
no alpha or transparency).
|
|
As of October 2026 the page marks "iPhone with Dynamic Island (medium display)" (1206 x 2622 / 1179 x 2556) as the
|
|
required iPhone class, and says a class without its own screenshots gets scaled ones from the next class: medium
|
|
from Face ID large (6.5"), 6.5" from Dynamic Island large (6.9"). So the 6.9" (+ 6.5") set still covers every
|
|
iPhone, and storebound keeps 6.9" as the default. For pixel-exact art on 6.3" phones (or if App Store Connect ever
|
|
refuses to submit without the medium set), set `apple.iphoneMedium: true`: the `iphone-medium` size key (1206 x 2622,
|
|
uploaded as `APP_IPHONE_61`, a mapping inferred from the enum, not verified) is then rendered, checked and uploaded
|
|
like the others. It stays off by default.
|
|
- ASC API `ScreenshotDisplayType`: https://developer.apple.com/documentation/appstoreconnectapi/screenshotdisplaytype
|
|
(there is no "13 inch" value: the 13" iPad set is `APP_IPAD_PRO_3GEN_129`).
|
|
- Play graphics: https://support.google.com/googleplay/android-developer/answer/9866151 (screenshots 320-3840 px, at
|
|
most 2:1, JPEG or 24-bit PNG, 8 MB; feature graphic 1024 x 500).
|
|
|
|
**iPad.** Set `apple.ipad: true` only if the app runs on iPad (iPhone-only apps skip the iPad set). Then the default
|
|
sizes include `ipad13` (3:4 layout), `apple screenshots` uploads it, `validate` requires it, and tablet frames default
|
|
to the 13" iPad screen (1032 x 1352 CSS px @2x below a 24 px bar), so an `iPad Pro 13-inch` / `iPad Air 13-inch`
|
|
simulator screenshot fits after the bar is cropped. Play tablet sizes (tab7 / tab10) reuse the same tablet frames, drawn
|
|
in a plain Android tablet body instead of the iPad one.
|
|
|
|
**Play tablets are optional.** Play requires the 512 x 512 icon, the 1024 x 500 feature graphic and at least 2
|
|
screenshots (answer 9866151 above); 7" / 10" tablet screenshots are optional, but without them the app is not eligible
|
|
for tablet featuring. `play.tablets` decides: unset = true when tablet renders (tab7/ or tab10/) already exist, else
|
|
false; `false` = `shots render`, `shots check`, `shots gallery`, `validate` and `play images` never ask for them;
|
|
`true` = they are rendered, and missing ones are errors. A phone-only project needs no setting: `play images` uploads
|
|
the icon, feature graphic and phone screenshots and prints `tablet screenshots: none (optional; ...)`.
|
|
|
|
## Steps
|
|
1. **Screenshot switches.** Give the app or game params for screenshots: language, demo data or a fixed seed, no ads /
|
|
analytics / purchase UI, skip onboarding or the tutorial, jump to a screen or level, freeze at a moment. Frames must
|
|
show the real app in use (store rule; for a game: real gameplay). Apps: sign in to a demo account with realistic,
|
|
non-personal sample data; never show real user data.
|
|
2. **Capture.** Configure `shots.capture` with a `source` (or pass `--source`), then
|
|
`storebound shots capture --lang en --dry` to see the plan, and without `--dry` to capture. Frames land in
|
|
`<shots.frames>/<lang>/<phone|tablet>/<shot>.png`, local, not committed.
|
|
- `web` (default): `baseUrl`, `query` with `{lang}`, shots: `path` / `waitFor` / `eval` / `delayMs` / `device`.
|
|
Phone 440 x 902 @3x and tablet 800 x 1240 @2x by default; the template draws the status bar above.
|
|
Speed (many languages): frames run in parallel browser contexts (`--concurrency N`, `capture.concurrency`;
|
|
default min(3, cores / 4) since game boots are GPU-heavy). `capture.session: 'per-language'` boots the app once
|
|
per language and device and switches shots in that page (per shot `navigate: 'spa'` = pushState + popstate to its
|
|
path, then `waitFor` / `eval`; `'reload'` = goto in the same page); a shot whose `waitFor` fails there within
|
|
`reuseTimeoutMs` (15000) gets a fresh page. Re-runs skip frames whose inputs did not change (shot entry, URL,
|
|
frame size, the language's copy, the build: `capture.build` files / folders, else the HTML and scripts at
|
|
`baseUrl`, fonts) via `.storebound/capture-cache.json`; `--only-missing` captures only missing files, `--force`
|
|
everything. Each progress line shows n/total, ETA and the time per step (goto, wait, eval, delay, shot); the end
|
|
lists the slowest frames and steps, and `.storebound/capture-report.json` keeps them. Put the slow step right
|
|
(a long `waitFor` = the game reaching the scene: add a URL switch that jumps there).
|
|
- `ios-sim` (Unity, Godot, Flutter, native, Capacitor: any Simulator build): `ios.device` (simulator name or UDID,
|
|
e.g. a 6.9" iPhone), `ios.bundleId`, optional `ios.app` (.app to install), `ios.args`. Per shot: `args` (launch
|
|
arguments, read them with UserDefaults / your engine) and / or `url` (deep link). Language goes in as
|
|
`-AppleLanguages` / `-AppleLocale`; status bar is forced to 9:41, full battery. After a cold boot the app is
|
|
launched once and given `ios.warmupMs` (default 8000) before the first shot; without it the first frame of a
|
|
fresh simulator is often blank. A simulator storebound boots is shut down afterwards (`--keep-booted` keeps it).
|
|
For iPad shots set `ios.devices.tablet` (e.g. "iPad Pro 13-inch (M5)") and `device: 'tablet' | 'both'` per shot.
|
|
- `android-emu`: `android.package`, optional `serial`, `activity`, `apk`. Per shot: `url` (VIEW intent) and / or
|
|
typed `extras`. Demo-mode status bar; per-app language on Android 13+ (older: pass `{lang}` in an extra).
|
|
Nothing is wiped or rebooted.
|
|
- `folder`: frames made any other way; `shots capture` (and `shots check`) only verify that every frame the scenes
|
|
use exists with the right aspect ratio and resolution.
|
|
`{lang}`, `{locale}`, `{shot}` are replaced in args, extras and URLs. Simulator / emulator screenshots lose
|
|
`cropTop` rows (the status bar) so they fit the template: by default the frame's bar share per device class
|
|
(162 px on a 6.9" iPhone, 48 px on a 13" iPad with `apple.ipad`); set `cropTop: 120` or `{ phone, tablet }` for
|
|
other devices, and `shots.phone` / `shots.tablet` to that device's screen in points. Capture warns when a frame's
|
|
aspect does not match. Give the app time to load: `delayMs` (simulator / emulator; 2500 default is short for a
|
|
cold start) or `waitFor` (web).
|
|
3. **Copy.** `store/shots/copy.json`: per language, per scene: `eyebrow`, `title` [white line, gold line], `sub`,
|
|
`badges` [[big, small]] (game style). Short and true (only claim what is in the app); about 15 Latin characters per title line.
|
|
Natural marketing tone per language, not literal translation; use the app's own terms.
|
|
4. **Fonts.** `storebound shots fonts` downloads title/body fonts and a Noto subset for every non-Latin script found in
|
|
the copy (CJK, Arabic, Hebrew, Devanagari, Thai, Cyrillic/Greek), then checks every character of every language
|
|
(copy.json + config languages) against the cmap of the fonts that draw it. A character the title / body font lacks
|
|
gets an automatic Noto fallback (`WARN tr title: ş ğ not in Lilita One -> Noto Sans Display`); no fallback at all
|
|
fails with the language and the characters. Renders that used an automatic fallback (or a system font) are ERRORS in
|
|
`shots check`: fix it with a title / body font that covers the language (Paytone One, Baloo 2, Rowdies for game
|
|
titles; Montserrat, Inter, Nunito for body), or, if the owner accepts mixed glyphs, `shots.fonts.fallback:
|
|
['Noto Sans Display']`. Lilita One lacks most of Latin Extended (Turkish ş ğ İ, Polish, Czech, Romanian) and
|
|
Vietnamese. Re-run after copy or font edits (render refuses fonts that no longer cover the copy).
|
|
A game's own brand font: put the file path (relative to the config; TTF / OTF / WOFF / WOFF2) or
|
|
`{ family, file, license }` into `shots.fonts.title` / `body` / `fallback`. The same coverage check runs on it, listed
|
|
fallbacks are tried before Noto (`pl title: ę ł ą Ł not in ER Display -> Noto Sans Display`), the file is served in
|
|
place (never copied) and its hash is in fonts.json, so edits to the font need `shots fonts` again. Ask the owner for
|
|
the license ('OFL-1.1', 'proprietary', ...) and put it in `license`; `shots check` warns without one.
|
|
5. **Style and scenes.** `shots.style`: `game` (sunburst, sparkles, outlined titles, badges) or `app` (calm gradient,
|
|
one clean device frame, headline + subline, no sparkles; badges only as quiet chips when the copy has them).
|
|
Default: `app` for the app / subscription-app profiles, `game` otherwise. `shots.scenes`: palette (game: fire,
|
|
violet, blast, green, night, sky, gold, candy; app: mist, paper, mint, sand, rose, sky, ocean, slate; or custom
|
|
`{ bg, glow, ink, shadow }`), frames (one front phone + optional back phones with x / dy / scale / rotate), badge
|
|
slots (y, side, icon, hot, stars). Fonts default to Paytone One + Montserrat (game) or Inter (app): all cover
|
|
Latin Extended and Vietnamese.
|
|
6. **Render.** (`--concurrency N`, default min(4, cores / 2) pages) `storebound shots render --lang en` first, review `store/renders/en/sheet.png` (contact sheet, sizes
|
|
packed into a grid), then
|
|
all languages in the background: `storebound shots render > .storebound/render.log 2>&1 &`.
|
|
7. **JPEG for Play.** `storebound shots jpeg` (q92, about 4x smaller than PNG; App Store stays PNG).
|
|
8. **Check.** `storebound shots check` (read-only, exit 1 on errors): pixel sizes (Apple: a listed size, portrait or
|
|
landscape; Play screenshots 320-3840 px at most 2:1; feature graphic exactly 1024 x 500), alpha, Play JPEG /
|
|
24-bit PNG and 8 MB, counts per set (Apple 1-10, Play 2-8), text overflow flags (warnings), glyphs drawn by an
|
|
automatic font fallback or a system font (errors; `shots render` measures every character in the page). `--variants` also checks
|
|
A/B variant folders. `storebound validate` runs the same checks on the renders folder and lists errors as
|
|
blockers next to the store state (`--no-local` skips them).
|
|
9. **Review.** `storebound shots gallery` writes `<renders>/index.html`: one self-contained page (no network, open it
|
|
from disk) with every language x size x scene, app previews with posters, variant tabs, filters (language, size,
|
|
only issues, search) and light / dark. Tiles with store check errors are outlined red, warnings amber, with the
|
|
message under the tile. Show the owner this page (or a screenshot of it) before uploading.
|
|
10. **Upload.** `storebound play images` and `storebound apple screenshots` (both idempotent, background + log).
|
|
`play images` uploads only the sets that exist (tablet / TV / Wear when rendered) and never deletes a set Play has
|
|
but the renders lack: the dry run shows `keeps <type> on Play`; `--prune` (only on the owner's word) deletes them.
|
|
|
|
## A/B variants and store experiments
|
|
`shots.variants.<id>`: `scenes` (order / subset), `copy` (a JSON merged over the base copy, only what changes),
|
|
`palettes` (per scene). `storebound shots render --variant b` (or `--variants` for all) writes
|
|
`<renders>/b/<lang>/...`; `shots check --variants`, `shots gallery` (tabs) and `shots jpeg --variant b` cover them.
|
|
`play images --variant b` / `apple screenshots --variant b` upload a variant to the MAIN listing (replacing the base
|
|
images there), so only do that after a test has a winner. What the stores offer, honestly:
|
|
- **Google Play store listing experiments** (Play Console > Grow users > Store listing experiments): up to 2 variants
|
|
against the current listing; one default-graphics experiment or up to 5 localized experiments at a time; icon,
|
|
feature graphic, screenshots (+ descriptions for localized ones); stops automatically after 6 months. Console only:
|
|
the Play Developer API has no experiments endpoint, so storebound renders and checks the variant images and the
|
|
agent uploads them in the experiment form with its browser tool; starting the experiment is the owner's button.
|
|
https://support.google.com/googleplay/android-developer/answer/6227309
|
|
- **App Store Product Page Optimization** (App Store Connect > your app > Product Page Optimization): up to 3
|
|
treatments against the original, one test at a time, up to 90 days; icon, screenshots and app previews, localized
|
|
per treatment; new metadata goes through App Review; alternate icons must ship in the binary; shown on iOS / iPadOS
|
|
15+. https://developer.apple.com/app-store/product-page-optimization/
|
|
Through the API with storebound (`appStoreVersionExperiments` v2, treatments, treatment localizations and their
|
|
screenshot sets):
|
|
1. `storebound shots render --variant b` (and `shots check --variants`, `shots gallery`).
|
|
2. `storebound apple ppo create --variant b --traffic 30 --dry`, then without `--dry`: one test with one treatment
|
|
per variant (named after it, max 3 variants: `--variant b,c`) and a localization per App Store locale.
|
|
3. `storebound apple ppo upload --variant b --dry`, then for real: the variant renders into the treatment's
|
|
screenshot sets per locale (MD5-idempotent). MCP: `apple_ppo_create`, `apple_ppo_upload`, `apple_ppo_status`.
|
|
4. **Owner**: `storebound apple ppo start` (asks first; treatments that are not approved yet go to App Review with
|
|
the next version), later `storebound apple ppo stop` (final: a stopped test cannot be restarted; stopping through
|
|
the API is `started: false`, not verified against a live test, so if it is refused stop it in App Store
|
|
Connect). Neither is an MCP tool. Results per treatment are only in App Store Connect.
|
|
The app must be live (Ready for Distribution); alternate icons and app preview treatments stay App Store Connect
|
|
clicks.
|
|
- Test one thing per experiment (usually the first screenshot or the order), give it real traffic, and do not read
|
|
results early.
|
|
|
|
## From an AI tool (MCP)
|
|
`storebound mcp` exposes `shots_check` (read-only) and `shots_capture`, `shots_render`, `shots_gallery` (local files,
|
|
`dry_run: true` by default: run it once dry, show the plan, then with `dry_run: false`). Uploads (`apple_screenshots`,
|
|
`play_images`, both with `variant`) are store writes: dry first, and only for real after the owner agreed. Rendering
|
|
many languages takes minutes: in a terminal prefer `storebound shots render > .storebound/render.log 2>&1 &`.
|
|
|
|
## Gotchas
|
|
- Output is 24-bit RGB PNG: Play rejects alpha / palette PNGs, Apple rejects alpha.
|
|
- Arabic: `rtl: true` on the language; numbers stay LTR inside RTL text automatically.
|
|
- Japanese breaks by phrase, Korean at spaces; long words shrink the line rather than wrap.
|
|
- Check every language's sheet for overflow before uploading; German / Russian titles run long.
|
|
- Play: 8 per type max; Apple: 10 per set max.
|
|
- Do not include prices, "free" badges for paid items, or other platforms' UI.
|