storebound/docs/store-art.md

12 KiB

Store art: screenshots and previews

Screenshots and previews are one step of the journey, but the one that takes the longest by hand, so storebound does a lot here. Two looks: shots.style: 'game' (sunburst, sparkles, badges) and shots.style: 'app' (calm gradient, a clean device frame, headline + subline). Everything below was made by storebound itself from two tiny fictional demos: a game ("Prism Puddle", procedural art) and an app ("Tidy Lists", one static page with sample data): raw frames captured from the web build, captions in several languages, every store size, a review gallery and an app preview video. No store account needed to try it.

App style: four calm screenshots and the feature graphic of the demo list app, rendered by storebound

Store screenshots rendered by storebound: four scenes in English plus the hero scene in Turkish, Japanese and Arabic

Play feature graphic App preview (App Store, 886 x 1920)
Play feature graphic, 1024 x 500 A few seconds of the app preview video

The review gallery: every language, size, scene and variant on one local page, store check issues highlighted

Contact sheet of one language (every size, packed into a grid)

Contact sheet: iPhone 6.9" and 6.5", 13" iPad, Play phone, 7" and 10" tablets, feature graphic

How it works

  1. Frames. Give your app or game a few switches (language, demo data or a fixed seed, no ads, jump to a screen, freeze, autoplay) and let shots capture take the frames: Playwright for a web build, simctl for anything that runs in the iOS Simulator (Unity, Godot, Flutter, React Native, native, Capacitor), adb for an Android emulator or device, or bring a folder of your own frames. The status bar is cropped off and redrawn clean (9:41, full battery). Web apps behind a login sign in first with shots.capture.auth: a sign-in request whose secrets come from .storebound/credentials.json or the environment (${cred:NAME}, ${env:NAME}; literal secrets are refused), or a session saved by storebound shots login, where you sign in to a demo account by hand in a fresh browser. Use a demo backend with fabricated data; the same frames feed the App Store and Google Play.

  2. Copy. Per language and scene: eyebrow, two-line title, subtitle, badges. Fonts are subset from Google Fonts to exactly your characters, so Japanese, Chinese, Korean, Arabic (right to left) and Hindi just work. shots fonts checks every character of every language against the font's cmap (TTF / OTF / WOFF / WOFF2, read in plain TypeScript): a glyph the font lacks gets an automatic Noto fallback (logged), no fallback is a clear error, and a render that drew any glyph with a fallback you did not choose, or with a system font, fails shots check.

    script title / body font (default stack)
    Latin incl. Latin Extended (tr, pl, cs, ro, hu ...) and Vietnamese your shots.fonts; default game Paytone One + Montserrat, app Inter (all cover them)
    Cyrillic, Greek Noto Sans (whole line)
    Arabic, Hebrew, Devanagari, Bengali, Thai Noto Sans Arabic / Hebrew / Devanagari / Bengali / Thai (whole line)
    Japanese, Korean, Chinese Noto Sans JP / KR / SC (TC for zh-Hant, TW, HK) (whole line)
    a character the stack lacks Noto Sans Display (titles) or Noto Sans (body), Noto Sans <Script>, then Noto Sans Symbols 2 / Symbols / Math: an ERROR in shots check unless listed in shots.fonts.fallback

    Some display fonts lack Latin Extended: Lilita One has no ş ğ İ ą ę ń ś ź ż ć ă ș ț č ř ě ů ő ű or Vietnamese, so Turkish, Polish, Czech, Romanian, Hungarian or Vietnamese titles need another title font (Paytone One, Baloo 2, Rowdies) or fonts: { ..., fallback: ['Noto Sans Display'] } to accept the mixed glyphs.

    Your own brand font. shots.fonts.title / body / fallback[] also take a local file (relative to the config; TTF, OTF, WOFF or WOFF2, checked by extension and signature) or { family, file, weight?, style?, license? }:

    fonts: {
      title: { family: 'ER Display', file: 'apps/game/public/assets/fonts/er-display.woff2', license: 'OFL-1.1' },
      body: 'Montserrat',
      fallback: ['Noto Sans Display'],   // tried in order, before the automatic Noto fallbacks
    },
    

    shots fonts runs the same cmap check on the file (pl title: ę ł ą Ł not in ER Display -> Noto Sans Display), prints its size, glyph count, sha256 and license, and records it in fonts.json. The file is never copied: render (and shots video captions) serve it from where it is, and a changed file changes fonts.css (the capture cache misses) and makes shots render ask for shots fonts again. shots check counts the local font and listed fallbacks as configured, not as fallbacks, and warns when a local font has no license note.

  3. Render. Every store size at exact pixels: iPhone 6.9" and 6.5", 13" iPad (if your app runs on iPad), Play phone, 7" and 10" tablets, the 1024 x 500 feature graphic. 24-bit RGB, no alpha (both stores reject it).

  4. Check and review. shots check applies the store rules locally (sources cited in cli/shots/spec.ts); shots gallery puts everything on one page with problems outlined. validate repeats the checks next to the store state.

  5. Variants. Other first screenshot, order or copy as shots.variants, rendered side by side, for a Play store listing experiment or an App Store Product Page Optimization test (set up in the consoles; storebound makes and checks the images).

  6. Video. shots record films the autoplay mode, shots video cuts 15-30 s App Store previews (886 x 1920, 1200 x 1600) and a 9:16 / 16:9 promo for YouTube with captions in every language, apple previews uploads them. Play takes a YouTube URL in play.video.

  7. iPhone Duo (foldable). apple.iphoneDuo: true renders and checks the outer (1398 x 2034) and inner (2007 x 2853) sets. Optional now, reportedly required from April 2027 for iOS 27.1 SDK builds (check / validate note it). apple screenshots uploads them through the App Asset Library (display class IPHONE_DUO). Play has no foldable slot: foldables use the 7" / 10" tablet sets.

Details: the store-screenshots and app-previews skills.


All docs · README

Mac, landscape and safe margins

  • Mac App Store: apple.mac: true renders mac (2880 x 1800; Apple takes the four 16:10 sizes 2880 x 1800, 2560 x 1600, 1440 x 900, 1280 x 800, no alpha), checks it, and apple screenshots --platform macos uploads it as APP_DESKTOP to the macOS version. validate notes a macOS version without Mac screenshots.
  • Landscape layout: copy on one side, the game or app on the other, in a Mac window (shots.landscape.frame: 'window') or bare ('none'). Used for mac, for Play's chromebook size (play.chromebook, console only: no API type) and, with shots.landscape.playTablets, for the Play 7" / 10" tablet slots. Frames: <frames>/<lang>/landscape/.
  • Safe margins: every template keeps text and logo 7 % of the width from the sides and 5 % of the height from the top; shots check measures the layout boxes recorded at render time and fails anything under 6 % / 4 %.

Palettes, photo backgrounds and title weight

  • palette.bg takes gradient stops (2-3 colours), one colour ('#15171C', flat) or a CSS gradient string (passed through). Anything else fails validation: "palette.bg must be a colour, a gradient or an array of 2-3 colours".
  • Photo backgrounds: palette.image (+ overlay, default 0.45) for scenes, shots.feature.background (a path or { image, overlay }) for the Play feature graphic. Cropped to cover under a flat dark overlay. Play's feature graphic can be a branded photo; the App Store has no feature graphic, but the same art fits the Header and Search Results slots below. shots check: the file exists and is at least the output size.
  • Contrast guard: shots render hides the title, samples the pixels behind it and records the worst ink contrast in report.json; shots check warns under 3:1.
  • Title weight: shots fonts fetches the weights the templates use (full wght axis, else the used range, else static weights); shots check warns when a title was drawn at a weight no loaded face serves (font-synthesis is off, so a 400-only face draws a 750 headline thin).

Header and Search Results (App Store creative assets, iOS / iPadOS 27+)

Since 5 Oct 2026 the App Store version page has a "Header and Search Results" tab next to "App Previews and Screenshots". Optional: without these assets the App Store uses in-app events, app previews and screenshots.

  • Files under the renders folder, per language: <renders>/<lang>/creative/header.png (3840 x 1646, 21:9), search.png (3:2, 1920 x 1280 up to 3840 x 2560), or one universal.png (5244 x 2950, 16:9) that fills either slot (also <renders>/creative/universal.png for every locale, or apple.creativeAssets.universal). Art without words needs no copy per locale: <renders>/creative/header.png / search.png fill every language without its own file. JPEG / PNG without alpha. Videos instead: header.mov|m4v|mp4 / search.*, 5-30 s, 30 or 60 fps, the same aspect ratio; search videos play muted. apple.creativeAssets.header: false / search: false skips a slot.
  • Not rendered by shots render: the scene templates are portrait device shots; make the wide art yourself (or from the feature graphic artwork at the larger size) and drop it in.
  • Upload and select: storebound apple creative-assets --dry, then without --dry. Files go into the app's Asset Library (API: appAssetLibraryImages / appAssetLibraryVideos, category CREATIVE_ASSETS; processing is a bounded wait of 2 minutes with a heartbeat). The version page's slots offer only approved assets ("Browse approved assets"), so an asset is placed (appAssetLibraryPlacements, PRODUCT_PAGE_HEADER_ASSET / APP_STORE_SEARCH_RESULTS_ASSET) once Apple approved it; until then each locale and placement says "uploaded , waiting for Apple's approval; run storebound apple creative-assets again later", and a later run places it. --verify, validate and the report show the same waiting state. Same file (MD5) = nothing sent; an archived copy is unarchived; a file App Review rejected is never uploaded again (change the file). MCP: apple_creative_assets (dry by default).
  • Review: an asset is reviewed only once it is in a review submission (its own item, appAssetLibraryImage / appAssetLibraryVideo; the console's "Add for Review"). --add-for-review adds the uploaded, unreviewed assets to the app's draft submission, once: before the first approved version it is the same submission as that version, later they can go alone. Pressing Submit for Review stays the owner's.
  • Live app, no editable version: the run uses the live version, where only APPROVED assets are selected (Apple swaps approved assets onto a live version without a new build). New files are uploaded and wait for review: --add-for-review sends them as a submission of their own (the app has an approved version), the owner presses Submit for Review, and a re-run after approval selects them.
  • Verify: storebound apple creative-assets --verify (MCP apple_creative_assets_verify) is read-only: every slot of the editable version (else the live one) shows the local file, with its review state; exit 1 when one differs.
  • Checks: storebound check and validate print an info note when no locale has one.
  • Play: no such slot. The feature graphic (1024 x 500, play images) and the promo video (YouTube URL) are the closest equivalents.