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:
parent
c03291e7f7
commit
3542b897c8
5 changed files with 21 additions and 8 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue