fix(landing-page): copy example.html sibling assets in post-build (#2880)

Some skill and design-template `example.html` files are thin shells that
iframe a neighbouring `./assets/<file>` (template HTML, mp4 showcases,
hero PNGs, etc.). The post-build copier only mirrored the entrypoint
file itself, so on Cloudflare Pages the asset path 404'd and the SPA
fallback served the OD homepage — users clicked "Click for live
preview" on /skills/after-hours-editorial-template/ and saw the landing
page rendered inside the iframe instead of the actual template.

Walk the entrypoint HTML for `(src|href|poster)="\./assets/..."` refs
and recursively mirror the sibling `assets/` directory only when one is
found. Most skills carry an `assets/` folder of reference PNGs the demo
never loads (open-design-landing alone is 22MB of unused mocks); a
relevance gate keeps the deploy from absorbing tens of MB per skill
that doesn't actually need them.

Six skills/templates currently match (after-hours-editorial-template,
8-bit-orbit-video-template, weread-year-in-review-video-template,
flowai-live-dashboard-template, trading-analysis-dashboard-template,
open-design-landing).

Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
This commit is contained in:
Jane 2026-05-26 14:12:56 +08:00 committed by GitHub
parent 5051f318ea
commit 2bab0c2add
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -30,7 +30,15 @@
* convention `generate-previews.ts` already uses.
*/
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync } from 'node:fs';
import {
cpSync,
existsSync,
mkdirSync,
copyFileSync,
readdirSync,
readFileSync,
statSync,
} from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -44,6 +52,7 @@ const LIVE_ARTIFACTS_SRC = path.join(REPO_ROOT, 'templates', 'live-artifacts');
let copied = 0;
let skipped = 0;
let assetDirsCopied = 0;
function copyIfExists(srcFile: string, destFile: string): boolean {
if (!existsSync(srcFile)) return false;
@ -52,6 +61,30 @@ function copyIfExists(srcFile: string, destFile: string): boolean {
return true;
}
// Some example.html / index.html files are thin shells that iframe a
// neighbouring `./assets/<file>` (template HTML, fonts, mp4 showcase,
// etc.). Without copying that sibling directory the iframe path 404s on
// Cloudflare Pages and SPA-fallbacks to the homepage, so the user sees
// the OD landing instead of the skill preview.
//
// Only mirror `assets/` when the entrypoint actually references it via
// a relative `./assets/...` URL — most skills carry an `assets/` folder
// of reference imagery (PNG mocks, design notes) that the demo never
// loads, and shipping those would inflate the deploy by tens of MB.
function copyReferencedAssetsDir(
entrypointHtml: string,
srcSlugDir: string,
destSlugDir: string,
): boolean {
if (!existsSync(entrypointHtml)) return false;
const html = readFileSync(entrypointHtml, 'utf8');
if (!/\b(?:src|href|poster)\s*=\s*["']\.\/assets\//i.test(html)) return false;
const assetsSrc = path.join(srcSlugDir, 'assets');
if (!existsSync(assetsSrc) || !statSync(assetsSrc).isDirectory()) return false;
cpSync(assetsSrc, path.join(destSlugDir, 'assets'), { recursive: true });
return true;
}
function listDirs(root: string): string[] {
if (!existsSync(root)) return [];
return readdirSync(root).filter((name) => {
@ -62,12 +95,16 @@ function listDirs(root: string): string[] {
// 1. Skills — `skills/<slug>/example.html` → `out/skills/<slug>/example.html`.
for (const slug of listDirs(SKILLS_SRC)) {
const ok = copyIfExists(
path.join(SKILLS_SRC, slug, 'example.html'),
path.join(OUT_DIR, 'skills', slug, 'example.html'),
);
if (ok) copied++;
else skipped++;
const srcDir = path.join(SKILLS_SRC, slug);
const destDir = path.join(OUT_DIR, 'skills', slug);
const entrypointSrc = path.join(srcDir, 'example.html');
const ok = copyIfExists(entrypointSrc, path.join(destDir, 'example.html'));
if (ok) {
copied++;
if (copyReferencedAssetsDir(entrypointSrc, srcDir, destDir)) assetDirsCopied++;
} else {
skipped++;
}
}
// 2. Design templates — `design-templates/<slug>/example.html` →
@ -76,11 +113,14 @@ for (const slug of listDirs(SKILLS_SRC)) {
// `_lib/catalog.ts` and `pages/templates/[slug]/index.astro` which
// routes skill-template-origin records to `/skills/<slug>/example.html`).
for (const slug of listDirs(DESIGN_TEMPLATES_SRC)) {
const ok = copyIfExists(
path.join(DESIGN_TEMPLATES_SRC, slug, 'example.html'),
path.join(OUT_DIR, 'skills', slug, 'example.html'),
);
if (ok) copied++;
const srcDir = path.join(DESIGN_TEMPLATES_SRC, slug);
const destDir = path.join(OUT_DIR, 'skills', slug);
const entrypointSrc = path.join(srcDir, 'example.html');
const ok = copyIfExists(entrypointSrc, path.join(destDir, 'example.html'));
if (ok) {
copied++;
if (copyReferencedAssetsDir(entrypointSrc, srcDir, destDir)) assetDirsCopied++;
}
}
// 3. Live-artifact templates — `templates/live-artifacts/<slug>/index.html`
@ -90,11 +130,16 @@ for (const slug of listDirs(DESIGN_TEMPLATES_SRC)) {
// serve `index.html` (the rendered preview) rather than
// `template.html` (raw template with `{{data.*}}` placeholders).
for (const slug of listDirs(LIVE_ARTIFACTS_SRC)) {
const ok = copyIfExists(
path.join(LIVE_ARTIFACTS_SRC, slug, 'index.html'),
path.join(OUT_DIR, 'templates', `live-${slug}`, 'preview.html'),
);
if (ok) copied++;
const srcDir = path.join(LIVE_ARTIFACTS_SRC, slug);
const destDir = path.join(OUT_DIR, 'templates', `live-${slug}`);
const entrypointSrc = path.join(srcDir, 'index.html');
const ok = copyIfExists(entrypointSrc, path.join(destDir, 'preview.html'));
if (ok) {
copied++;
if (copyReferencedAssetsDir(entrypointSrc, srcDir, destDir)) assetDirsCopied++;
}
}
console.log(`[copy-example-html] copied ${copied} files, skipped ${skipped} (no preview source in repo)`);
console.log(
`[copy-example-html] copied ${copied} files (+ ${assetDirsCopied} assets/ dirs), skipped ${skipped} (no preview source in repo)`,
);