creative assets: <renders>/creative/header.* / search.* fill every language without its own file (art without words needs no copy per locale; one upload, placed per locale)

This commit is contained in:
CRTkafa 2026-10-19 05:47:27 +07:00
parent c03291e7f7
commit 3542b897c8
5 changed files with 21 additions and 8 deletions

View file

@ -11,7 +11,8 @@
read-only and fails when one differs from the local file. A file App Review rejected, or one that failed processing
twice, is not uploaded again; an archived copy of the same file is unarchived instead of uploaded. A live app without
an editable version uses the live version: approved assets are selected there, new ones are uploaded and wait for a
review of their own. Pitfall 76 rewritten.
review of their own. Art without words needs no copy per locale: `<renders>/creative/header.png` / `search.*`
fill every language without its own file. Pitfall 76 rewritten.
## 0.8.13 · 2026-10-18

View file

@ -2,8 +2,9 @@
// 27+): the product page header (21:9) and the search results asset (3:2), image or video, per locale, with one
// universal 16:9 image (5244 x 2950) that can fill either slot. Apple's Creative assets specifications:
// https://developer.apple.com/help/app-store-connect/reference/app-information/creative-assets-specifications/
// Files: <renders>/<language id>/creative/{header,search}.{png,jpg,jpeg,mov,m4v,mp4}; universal fallback
// <renders>/<language id>/creative/universal.png or <renders>/creative/universal.png (every locale).
// Files: <renders>/<language id>/creative/{header,search}.{png,jpg,jpeg,mov,m4v,mp4}, else the same names in
// <renders>/creative/ for every locale; universal fallback <renders>/<language id>/creative/universal.png or
// <renders>/creative/universal.png (every locale).
// API: App Asset Library (placement types PRODUCT_PAGE_HEADER_ASSET and APP_STORE_SEARCH_RESULTS_ASSET); see
// cli/apple/creative-assets.ts. Play has no such slot: the feature graphic (1024 x 500) and the promo video are its
// closest equivalents.
@ -29,16 +30,20 @@ export interface CreativeFile { placement: Placement; file: string; kind: 'image
const EXT = /\.(png|jpe?g|mov|m4v|mp4)$/i;
const kindOf = (f: string): 'image' | 'video' => (CREATIVE_RULES.video.ext.test(f) ? 'video' : 'image');
/** The file for each placement of one language: own header / search file first, else the universal image. */
/** The file for each placement of one language: its own header / search file first, then a shared one for every
* language (<renders>/creative/header.png: art without words needs no copy per locale), then the universal image. */
export function creativeFiles(renders: string, lang: string, list: (d: string) => string[] = d => (existsSync(d) ? readdirSync(d) : [])): CreativeFile[] {
const dir = join(renders, lang, 'creative');
const dir = join(renders, lang, 'creative'), shared = join(renders, 'creative');
const own = list(dir).filter(f => EXT.test(f)).sort();
const common = list(shared).filter(f => EXT.test(f)).sort();
const uni = own.find(f => /^universal\.png$/i.test(f)) ? join(dir, own.find(f => /^universal\.png$/i.test(f))!)
: list(join(renders, 'creative')).some(f => f === 'universal.png') ? join(renders, 'creative', 'universal.png') : undefined;
: common.includes('universal.png') ? join(shared, 'universal.png') : undefined;
const out: CreativeFile[] = [];
for (const p of ['header', 'search'] as Placement[]) {
const f = own.find(x => x.toLowerCase().startsWith(`${p}.`));
const g = common.find(x => x.toLowerCase().startsWith(`${p}.`));
if (f) out.push({ placement: p, file: join(dir, f), kind: kindOf(f), universal: false });
else if (g) out.push({ placement: p, file: join(shared, g), kind: kindOf(g), universal: false });
else if (uni) out.push({ placement: p, file: uni, kind: 'image', universal: true });
}
return out;

View file

@ -93,7 +93,8 @@ Since 5 Oct 2026 the App Store version page has a "Header and Search Results" ta
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`). JPEG / PNG without
(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

View file

@ -202,7 +202,8 @@ Since 5 Oct 2026 the App Store version page has a "Header and Search Results" ta
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`). JPEG / PNG without
(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

View file

@ -53,6 +53,11 @@ describe('creative asset checks', () => {
const fs = creativeFiles('/r', 'en', list);
expect(fs.map(f => [f.placement, f.file, f.universal])).toEqual([['header', '/r/en/creative/header.png', false], ['search', '/r/creative/universal.png', true]]);
});
test('files: a shared <renders>/creative/header.png fills every language without its own', () => {
const list = (d: string) => (d === '/r/creative' ? ['header.png', 'search.mp4', 'universal.png'] : d === '/r/tr/creative' ? ['search.png'] : []);
expect(creativeFiles('/r', 'en', list).map(f => [f.placement, f.file, f.kind, f.universal])).toEqual([['header', '/r/creative/header.png', 'image', false], ['search', '/r/creative/search.mp4', 'video', false]]);
expect(creativeFiles('/r', 'tr', list).map(f => f.file)).toEqual(['/r/creative/header.png', '/r/tr/creative/search.png']);
});
test('info note: an App Store app without any asset; none for Play-only or with files', () => {
const langs = [{ id: 'en', apple: 'en-US' }];
expect(creativeNote({ apple: {}, languages: langs }, '/r', () => [])).toBe(CREATIVE_NOTE);