index + purpose sections, full asset fingerprinting, credit footer

This commit is contained in:
2026-08-17 15:24:22 +10:00
parent 5a56ed6434
commit 8b2a37d840
2 changed files with 74 additions and 15 deletions
+27 -13
View File
@@ -3,7 +3,8 @@
// Reads pilot/arc.tpl.html, splits the app <script> out to arc.js, injects the scene
// manifests, points heroes at /assets/ files, and wraps a full HTML document.
// Runs on bare node (the build image has no python). Artifact build = assemble-arc.py.
import { readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -34,13 +35,32 @@ const heroes = {
__HERO_HELM__: "course-VIII-helm-press.webp",
__HERO_APPENDIX__: "appendix-dependency-ledger.webp",
};
for (const [ph, file] of Object.entries(heroes)) body = body.replace(ph, "/assets/" + file);
body = body.replace("__DIAG_FLOW__", "/assets/delivery-flow.svg");
// content-fingerprint everything under /assets/ (immutable-cached by nginx): a fresh
// index.html can then never pair with a stale script or image from a previous deploy.
mkdirSync(join(DIST, "assets"), { recursive: true });
const fp = buf => createHash("sha256").update(buf).digest("hex").slice(0, 8);
const emit = (srcPath, base, ext) => {
const buf = readFileSync(srcPath);
const name = `${base}.${fp(buf)}.${ext}`;
writeFileSync(join(DIST, "assets", name), buf);
return "/assets/" + name;
};
for (const [ph, file] of Object.entries(heroes)) {
const base = file.replace(/\.webp$/, "");
body = body.replace(ph, emit(join(ROOT, "assets/dist", file), base, "webp"));
}
body = body.replace("__DIAG_FLOW__", emit(join(ROOT, "assets/dist/delivery-flow.svg"), "delivery-flow", "svg"));
if (body.includes("__HERO_") || body.includes("__DIAG_") || app.includes("__MAN_")) throw new Error("unfilled placeholder");
const favicon = "data:image/svg+xml," + encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#070b14"/><rect x="6" y="4" width="14" height="7" rx="1.5" fill="#3fbaf5"/><rect x="11" y="13" width="14" height="7" rx="1.5" fill="#7dd0f9"/><rect x="6" y="22" width="14" height="7" rx="1.5" fill="#e879f9"/></svg>`);
const gsapRef = emit(join(ROOT, "gsap.min.js"), "gsap.min", "js");
const stRef = emit(join(ROOT, "st.min.js"), "st.min", "js");
const appName = `arc.${fp(app)}.js`;
writeFileSync(join(DIST, "assets", appName), app);
const appRef = "/assets/" + appName;
const html = `<!doctype html>
<html lang="en">
<head>
@@ -53,22 +73,16 @@ const html = `<!doctype html>
</head>
<body>
${body}
<script src="/gsap.min.js"></script>
<script src="/st.min.js"></script>
<script src="/arc.js"></script>
<script src="${gsapRef}"></script>
<script src="${stRef}"></script>
<script src="${appRef}"></script>
</body>
</html>
`;
mkdirSync(join(DIST, "assets"), { recursive: true });
writeFileSync(join(DIST, "index.html"), html);
writeFileSync(join(DIST, "arc.js"), app);
copyFileSync(join(ROOT, "gsap.min.js"), join(DIST, "gsap.min.js"));
copyFileSync(join(ROOT, "st.min.js"), join(DIST, "st.min.js"));
for (const f of Object.values(heroes)) copyFileSync(join(ROOT, "assets/dist", f), join(DIST, "assets", f));
copyFileSync(join(ROOT, "assets/dist/delivery-flow.svg"), join(DIST, "assets/delivery-flow.svg"));
// CSP gate: no inline <script> may survive into the served HTML
const inline = (html.match(/<script(?![^>]*src=)[^>]*>/gi) || []).length;
if (inline) throw new Error("inline <script> in index.html - CSP would block it");
console.log(`site build -> ${DIST}: index.html ${(html.length / 1024).toFixed(0)}KB + arc.js + 2 bundles + ${Object.keys(heroes).length} heroes`);
console.log(`site build -> ${DIST}: index.html ${(html.length / 1024).toFixed(0)}KB, all assets fingerprinted (${appName})`);