Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions design-templates/open-design-landing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,13 @@ Set `inputs.imagery.strategy` accordingly.
npx tsx scripts/placeholder.ts <out>/assets/
```

Writes 16 `.svg` files (with `.png` aliases for compatibility) into
`<out>/assets/`. Each placeholder shows the slot id, ratio, pixel
dimensions, and the prompt hint from `image-manifest.json`. The
composer's `<img src='./assets/hero.png'>` etc. just work.
Writes 16 `.svg` files into `<out>/assets/`. Each placeholder shows the
slot id, ratio, pixel dimensions, and the prompt hint from
`image-manifest.json`. The composer references these as `./assets/hero.svg`
etc. — it swaps the extension to `.png` for the `generate` /
`bring-your-own` strategies, so placeholders and real imagery never mix
extensions (a PNG-named SVG would break in browsers that sniff by
extension).

#### `generate` — gpt-image-2 mode

Expand Down
45 changes: 29 additions & 16 deletions design-templates/open-design-landing/scripts/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ function renderNav(i: EditorialCollageInputs): string {
</header>`;
}

function imageUrl(
assets: string,
name: string,
strategy: EditorialCollageInputs['imagery']['strategy'],
): string {
// #5903: placeholder imagery is emitted as .svg — a PNG-named file whose
// bytes are SVG XML breaks in browsers that sniff by extension. Real
// generated/user PNGs keep the .png extension.
return `${assets}${name}.${strategy === 'placeholder' ? 'svg' : 'png'}`;
}

function renderSecRule(r: SectionRule): string {
return `
<div class='sec-rule'>
Expand Down Expand Up @@ -234,7 +245,7 @@ function renderHero(i: EditorialCollageInputs): string {
<span class='annot annot-tr'>${i.hero.annotations.tr}</span>
<span class='annot annot-bl coord'>${i.hero.annotations.bl}</span>
<span class='annot annot-br'>${i.hero.annotations.br}</span>
<img src='${assets}hero.png' alt='' />
<img src='${imageUrl(assets, 'hero', i.imagery.strategy)}' alt='' />
<div class='index'>
${index}
</div>
Expand Down Expand Up @@ -269,7 +280,7 @@ function renderAbout(i: EditorialCollageInputs): string {
</div>
</div>
<div class='about-art' data-reveal='right'>
<img src='${assets}about.png' alt='' />
<img src='${imageUrl(assets, 'about', i.imagery.strategy)}' alt='' />
<div class='about-side-note'>
<b></b>
${i.about.side_note}
Expand All @@ -284,7 +295,7 @@ function renderAbout(i: EditorialCollageInputs): string {
</section>`;
}

function renderCapabilityCard(c: CapabilityCard): string {
function renderCapabilityCard(c: CapabilityCard, strategy: EditorialCollageInputs['imagery']['strategy']): string {
return `<div class='card' data-reveal>
<div class='num'>${c.num}<span class='tag'>${c.tag}</span></div>
<svg class='icon' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.5'>
Expand All @@ -299,7 +310,9 @@ function renderCapabilityCard(c: CapabilityCard): string {
}

function renderCapabilities(i: EditorialCollageInputs): string {
const cards = i.capabilities.cards.map(renderCapabilityCard).join('\n ');
const cards = i.capabilities.cards
.map((c) => renderCapabilityCard(c, i.imagery.strategy))
.join('\n ');
const assets = i.imagery.assets_path.replace(/\/?$/, '/');
return `
<section class='capabilities' id='agents' data-od-id='capabilities'>
Expand All @@ -309,7 +322,7 @@ function renderCapabilities(i: EditorialCollageInputs): string {
<div class='capabilities-art' data-reveal='left'>
<span class='corner tl'></span>
<span class='corner br'></span>
<img src='${assets}capabilities.png' alt='' />
<img src='${imageUrl(assets, 'capabilities', i.imagery.strategy)}' alt='' />
<div class='ribbon'>${i.capabilities.ribbon}</div>
</div>
<div class='capabilities-copy' data-reveal>
Expand All @@ -329,9 +342,9 @@ function renderLabPill(p: LabPill): string {
return `<button class='pill${p.active ? ' active' : ''}'>${p.label}<span class='count'>${p.count}</span></button>`;
}

function renderLabCard(c: LabCard, n: number, assets: string): string {
function renderLabCard(c: LabCard, n: number, assets: string, strategy: EditorialCollageInputs['imagery']['strategy']): string {
return `<div class='lab' data-reveal>
<div class='lab-img'><span class='badge'>${c.badge}</span><img src='${assets}lab-${n}.png' alt='' /></div>
<div class='lab-img'><span class='badge'>${c.badge}</span><img src='${imageUrl(assets, `lab-${n}`, strategy)}' alt='' /></div>
<div class='num-row'><span>${c.num}</span><span>${c.year}</span></div>
<h4>${c.title}</h4>
<p>${c.body}</p>
Expand All @@ -343,7 +356,7 @@ function renderLabs(i: EditorialCollageInputs): string {
const pills = i.labs.pills.map(renderLabPill).join('\n ');
const assets = i.imagery.assets_path.replace(/\/?$/, '/');
const cards = i.labs.cards
.map((c, idx) => renderLabCard(c, idx + 1, assets))
.map((c, idx) => renderLabCard(c, idx + 1, assets, i.imagery.strategy))
.join('\n ');
const progress = Array.from({ length: i.labs.progress.total }, (_, k) =>
k < i.labs.progress.filled ? `<span class='on'></span>` : `<span></span>`,
Expand Down Expand Up @@ -381,19 +394,19 @@ function renderLabs(i: EditorialCollageInputs): string {
</section>`;
}

function renderMethodStep(s: MethodStep, last: boolean, n: number, assets: string): string {
function renderMethodStep(s: MethodStep, last: boolean, n: number, assets: string, strategy: EditorialCollageInputs['imagery']['strategy']): string {
return `<div class='method-step' data-reveal>
<div class='num'>${s.num}</div>
<h4>${s.title}${last ? '' : ` <span class='arrow-r'>→</span>`}</h4>
<p>${s.body}</p>
<div class='img'><img src='${assets}method-${n}.png' alt='' /></div>
<div class='img'><img src='${imageUrl(assets, `method-${n}`, strategy)}' alt='' /></div>
</div>`;
}

function renderMethod(i: EditorialCollageInputs): string {
const assets = i.imagery.assets_path.replace(/\/?$/, '/');
const steps = i.method.steps
.map((s, idx, arr) => renderMethodStep(s, idx === arr.length - 1, idx + 1, assets))
.map((s, idx, arr) => renderMethodStep(s, idx === arr.length - 1, idx + 1, assets, i.imagery.strategy))
.join('\n ');
return `
<section class='method' data-od-id='method'>
Expand Down Expand Up @@ -423,15 +436,15 @@ function renderMethod(i: EditorialCollageInputs): string {
</section>`;
}

function renderWorkCard(c: WorkCard, idx: number, assets: string, href: string): string {
function renderWorkCard(c: WorkCard, idx: number, assets: string, href: string, strategy: EditorialCollageInputs['imagery']['strategy']): string {
return `<a class='work-card${idx === 1 ? ' alt' : ''}' data-reveal href='${href}'${ext(href)}>
<div class='label-row'>
<span class='small-label'>${c.small_label}</span>
<span class='index'>${c.index}</span>
</div>
<h3>${c.title}</h3>
<p>${c.body}</p>
<div class='img'><img src='${assets}work-${idx + 1}.png' alt='' /></div>
<div class='img'><img src='${imageUrl(assets, `work-${idx + 1}`, strategy)}' alt='' /></div>
<div class='meta-row'>
<span class='year'>${c.year}</span>
<span>${c.tag}</span>
Expand All @@ -445,7 +458,7 @@ function renderWork(i: EditorialCollageInputs): string {
// Use the first nav link as the work-card href fallback (we don't model per-card hrefs in WorkCard).
const fallbackHref = i.nav.find((l) => /skills/i.test(l.label))?.href ?? '#';
const cards = i.work.cards
.map((c, idx) => renderWorkCard(c, idx, assets, fallbackHref))
.map((c, idx) => renderWorkCard(c, idx, assets, fallbackHref, i.imagery.strategy))
.join('\n ');
return `
<section class='tight' data-od-id='work'>
Expand Down Expand Up @@ -516,7 +529,7 @@ function renderTestimonial(i: EditorialCollageInputs): string {
<a class='read-more' href='${i.testimonial.read_more_href}'${ext(i.testimonial.read_more_href)}>${i.testimonial.read_more_label}</a>
</div>
<div class='testimonial-art' data-reveal='right'>
<img src='${assets}testimonial.png' alt='' />
<img src='${imageUrl(assets, 'testimonial', i.imagery.strategy)}' alt='' />
</div>
</div>
</div>
Expand Down Expand Up @@ -551,7 +564,7 @@ function renderCTA(i: EditorialCollageInputs): string {
</div>
</div>
<div class='cta-art' data-reveal='right'>
<img src='${assets}cta.png' alt='' />
<img src='${imageUrl(assets, 'cta', i.imagery.strategy)}' alt='' />
<div class='index'>Nº 08</div>
<div class='ribbon'>${i.cta.ribbon}</div>
</div>
Expand Down
37 changes: 13 additions & 24 deletions design-templates/open-design-landing/scripts/placeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
* open-design-landing — SVG framework placeholder generator.
*
* When `imagery.strategy === 'placeholder'`, this script writes one
* paper-textured SVG file per slot in `assets/image-manifest.json`.
* The generated files live alongside the schema-named PNGs that the
* composer references (`hero.png`, `about.png`, `lab-1.png`, …) so
* the layout renders fully without any image budget.
* paper-textured SVG file per slot in `assets/image-manifest.json`,
* named `<id>.svg`. The composer references slot imagery by a
* strategy-aware extension (`.svg` here, `.png` for real generated or
* user imagery), so the layout renders fully without any image budget.
*
* Each placeholder shows: slot id · ratio · pixel dimensions · the
* `prompt_section` hint copied from the manifest. Drop the real PNG
* with the same filename to swap in production imagery; no markup
* with the same base name to swap in production imagery; no markup
* change required.
*
* Usage:
Expand Down Expand Up @@ -124,19 +124,12 @@ async function loadManifest(): Promise<Manifest> {
}

/**
* Write `<out>/<slot.file>` for every slot. The composer references
* slots by .png filename; we honor that by writing `<basename>.svg`
* AND a `<basename>.png.svg` symlink-style fallback. Most static
* hosts serve SVG to <img> just fine, so the practical convention
* is: if you want placeholders, point your `imagery.assets_path` at
* a directory of `.svg` files OR rename the SVGs to `.png` (some
* browsers honor extensionless content-sniffing).
*
* For the most reliable result, write BOTH:
* - `<id>.svg` — clean, editable
* - `<file>` — same SVG content under the .png filename so the
* composer's `<img src='./assets/<id>.png'>` works
* without changing markup.
* Write `<out>/<id>.svg` for every slot. The composer references slot imagery
* by a strategy-aware extension: `.svg` for the `placeholder` strategy (this
* script) and `.png` for real generated/user imagery. Writing the SVG payload
* under a `.png` filename — the old "png alias for compatibility" behavior —
* produced broken images, because browsers sniff `.png` by extension and refuse
* the SVG bytes (#5903).
*/
export async function writePlaceholders(outDir: string): Promise<string[]> {
const manifest = await loadManifest();
Expand All @@ -145,10 +138,8 @@ export async function writePlaceholders(outDir: string): Promise<string[]> {
for (const slot of manifest.slots) {
const svg = placeholderSvg(slot);
const svgPath = resolve(outDir, `${slot.id}.svg`);
const pngPath = resolve(outDir, slot.file);
await writeFile(svgPath, svg, 'utf8');
await writeFile(pngPath, svg, 'utf8');
written.push(svgPath, pngPath);
written.push(svgPath);
}
return written;
}
Expand All @@ -159,9 +150,7 @@ async function main(): Promise<void> {
? outArg!
: resolve(process.cwd(), outArg ?? './assets/');
const written = await writePlaceholders(out);
const pngs = written.filter((p) => p.endsWith('.png')).length;
const svgs = written.filter((p) => p.endsWith('.svg')).length;
console.log(`✓ wrote ${pngs} png-named placeholders + ${svgs} svg files into ${out}`);
console.log(`✓ wrote ${written.length} svg placeholders into ${out}`);
console.log(` (${written.map((p) => basename(p)).join(', ')})`);
}

Expand Down
Loading