Files
exploded-cluster/pilot/assemble-arc.mjs
T
jwright f6616a5863 learn site: one shared masthead for every page
The three pages had each grown their own tokens, type scale and alignment, so
the openings looked like three different sites. The document shell now owns the
masthead family - tokens, centred masthead, eyebrow, title, lede, fact strip,
scroll cue and the index component - and the pages carry only their content.
The Mirror gains the section index the arc has, drops its duplicate technical
footer, and its title now uses the same magenta accent instead of a one-off
gradient.
2026-08-25 19:07:48 +10:00

348 lines
19 KiB
JavaScript

#!/usr/bin/env node
// Site build for the delivery-arc page (CSP-clean: script-src 'self', no inline JS).
// Reads pilot/arc.tpl.html, splits the app <script> out to a fingerprinted arc.js, injects
// the scene manifests, fingerprints every /assets/ file, sets intrinsic image dimensions,
// wraps a full HTML document, writes build.json, and runs preflight gates.
// Runs on bare node (the build image has no python). Artifact build = assemble-arc.py.
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const DIST = process.argv[2] || join(ROOT, "dist");
const SITE = "https://learn.bztmon.com";
const BUILD_REV = process.env.BUILD_REV || "dev";
const man = t => JSON.stringify(JSON.parse(readFileSync(join(ROOT, "pilot", `manifest-${t}.json`), "utf8")));
// --- intrinsic WebP dimensions (VP8 / VP8L / VP8X), so the browser reserves layout space ---
function webpSize(buf) {
if (buf.length < 30 || buf.toString("ascii", 0, 4) !== "RIFF") return null;
const fourcc = buf.toString("ascii", 12, 16);
try {
if (fourcc === "VP8 ") {
return { w: (buf.readUInt16LE(26) & 0x3fff), h: (buf.readUInt16LE(28) & 0x3fff) };
}
if (fourcc === "VP8L") {
const b = buf; const o = 21;
const w = 1 + (((b[o + 1] & 0x3f) << 8) | b[o]);
const h = 1 + (((b[o + 3] & 0x0f) << 10) | (b[o + 2] << 2) | ((b[o + 1] & 0xc0) >> 6));
return { w, h };
}
if (fourcc === "VP8X") {
const o = 24;
const w = 1 + (buf[o] | (buf[o + 1] << 8) | (buf[o + 2] << 16));
const h = 1 + (buf[o + 3] | (buf[o + 4] << 8) | (buf[o + 5] << 16));
return { w, h };
}
} catch { /* fall through */ }
return null;
}
// --- intrinsic JPEG dimensions (SOF0/SOF2 markers) for the mirror plates ---
function jpegSize(buf) {
if (buf[0] !== 0xff || buf[1] !== 0xd8) return null;
let o = 2;
while (o + 9 < buf.length) {
if (buf[o] !== 0xff) { o++; continue; }
const marker = buf[o + 1];
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
return { h: buf.readUInt16BE(o + 5), w: buf.readUInt16BE(o + 7) };
}
o += 2 + buf.readUInt16BE(o + 2);
}
return null;
}
// skip link: emitted by the shared shell, so its accessibility CSS lives in the shell
// too - visually hidden until keyboard focus, visible and prominent when focused.
// ---------------------------------------------------------------------------
// Shared masthead: ONE opening treatment for every page on the site. Pages used
// to declare their own tokens, type scale and alignment, which is why the three
// headers drifted apart. This block owns the family - tokens, the centred
// masthead, eyebrow, title, lede and scroll cue - and pages simply use it.
// ---------------------------------------------------------------------------
const MAST_CSS = `<style>
:root{
--void:#070b14; --ink:#c9d7ef; --dim:#7286a8; --ink-dim:#7286a8; --line:#1b2740;
--cyan:#3fbaf5; --magenta:#e879f9; --amber:#e8b44a;
--dur-fast:.2s; --dur-base:.35s; --dur-slow:.6s;
--sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
.masthead{position:relative;max-width:64ch;margin:0 auto;padding:16vh 24px 9vh;text-align:center}
.masthead > :not(.introbg){position:relative}
.chapnav{font-family:var(--mono);font-size:.72rem;letter-spacing:.14em;margin:0 0 2.2rem}
.chapnav a{color:var(--dim);text-decoration:none;border-bottom:1px solid transparent;
padding-bottom:2px;transition:color var(--dur-fast) ease,border-color var(--dur-fast) ease}
.chapnav a:hover,.chapnav a:focus-visible{color:var(--cyan);border-color:var(--cyan)}
.eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
color:var(--cyan);margin:0 0 1.5rem}
.masthead h1,.thesis{font-size:clamp(1.9rem,4.8vw,3.2rem);line-height:1.1;margin:0 0 1.3rem;
font-weight:600;letter-spacing:-.02em;text-wrap:balance;font-family:var(--sans);color:var(--ink)}
.masthead h1 em,.thesis em{font-style:normal;color:var(--magenta)}
.lede{color:var(--dim);font-size:1.02rem;line-height:1.7;margin:0 auto;max-width:58ch}
.scrollcue{font-family:var(--mono);font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
color:var(--dim);margin-top:4.5rem;display:flex;flex-direction:column;align-items:center;gap:.7rem}
.scrollcue span{display:block;width:1px;height:46px;background:linear-gradient(var(--cyan),transparent)}
.mastfacts{display:flex;flex-wrap:wrap;justify-content:center;gap:0;margin:2.6rem auto 0;
border-top:1px solid var(--line);border-bottom:1px solid var(--line);max-width:52ch}
.mastfacts div{flex:1 1 8rem;padding:.85rem 1rem;border-right:1px solid var(--line)}
.mastfacts div:last-child{border-right:0}
.mastfacts dt{font-family:var(--mono);font-size:.64rem;letter-spacing:.16em;text-transform:uppercase;
color:var(--dim);margin:0 0 .3rem}
.mastfacts dd{margin:0;font-family:var(--mono);font-size:.88rem;color:var(--cyan)}
.toc{max-width:760px;margin:0 auto;padding:0 24px 5vh}
.toc .kt{font-family:var(--mono);font-size:.68rem;letter-spacing:.16em;
text-transform:uppercase;color:var(--dim);text-align:center;margin:0 0 1rem}
.toc ol{list-style:none;margin:0;padding:0;counter-reset:toc;display:flex;flex-direction:column;gap:8px}
.toc li{counter-increment:toc}
.toc a{display:grid;grid-template-columns:34px 110px 1fr;gap:12px;align-items:baseline;
border:1px solid var(--line);border-radius:8px;padding:10px 16px;background:#0a1020;text-decoration:none}
.toc a::before{content:counter(toc,decimal-leading-zero);font-family:var(--mono);font-size:.68rem;color:var(--dim)}
.toc a:hover,.toc a:focus-visible{border-color:var(--cyan)}
.toc b{font-family:var(--mono);font-size:.72rem;letter-spacing:.08em;color:var(--cyan);font-weight:600}
.toc span{font-size:.82rem;line-height:1.5;color:var(--dim)}
@media (max-width:640px){.toc a{grid-template-columns:34px 1fr}.toc span{grid-column:2}}
@media (max-width:900px){.masthead{padding:9vh 22px 6vh}}
</style>`;
const SKIP_CSS = `<style>
a.skip{position:absolute;left:-9999px;top:0;z-index:99}
a.skip:focus{left:1rem;top:1rem;background:#070b14;color:#3fbaf5;
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8rem;
padding:10px 16px;border:1px solid #3fbaf5;border-radius:6px;outline:none}
</style>`;
let tpl = readFileSync(join(ROOT, "pilot/arc.tpl.html"), "utf8");
// split the app script (the last <script> block) out of the template
const i = tpl.lastIndexOf("<script>");
const j = tpl.lastIndexOf("</script>");
if (i < 0 || j < i) throw new Error("app script block not found");
let app = tpl.slice(i + 8, j)
.replace("__MAN_SURROUND__", man("surround"))
.replace("__MAN_NODE__", man("node"))
.replace("__MAN_POD__", man("pod"))
.replace("__MAN_TRAFFIC__", man("traffic"))
.replace("__MAN_ORBIT__", man("orbit"))
.replace("__MAN_DOCKER__", man("docker"))
.replace("__MAN_CLUSTER__", man("cluster"))
.replace("__MAN_GITOPS__", man("gitops"))
.replace("__MAN_SUPPLY__", man("supply"))
.replace("__MAN_HELM__", man("helm"));
let body = tpl.slice(0, i) + tpl.slice(j + 9);
const heroes = {
__HERO_INTRO__: "intro-establishing.webp",
__HERO_SURROUND__: "course-00-surround.webp",
__HERO_NODE__: "course-III-node.webp",
__HERO_POD__: "course-III-pod.webp",
__HERO_TRAFFIC__: "course-IV-traffic.webp",
__HERO_ORBIT__: "course-V-orbit.webp",
__HERO_DOCKER__: "docker-layers.webp",
__HERO_CLUSTER__: "course-II-cluster.webp",
__HERO_GITOPS__: "course-VI-gitops.webp",
__HERO_SUPPLY__: "course-VII-supply-chain.webp",
__HERO_HELM__: "course-VIII-helm-press.webp",
__HERO_APPENDIX__: "appendix-dependency-ledger.webp",
};
// 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 { url: "/assets/" + name, buf };
};
const heroUrls = {};
for (const [ph, file] of Object.entries(heroes)) {
const base = file.replace(/\.webp$/, "");
const { url, buf } = emit(join(ROOT, "assets/dist", file), base, "webp");
heroUrls[ph] = url;
const dims = webpSize(buf);
const dimAttr = dims ? ` width="${dims.w}" height="${dims.h}"` : "";
// The intro backdrop is the above-the-fold LCP candidate -> eager. Everything else,
// including Course 00's hero, sits below the fold and lazy-loads.
const load = ph === "__HERO_INTRO__" ? "eager" : "lazy";
body = body.replace(`src="${ph}"`, `src="${url}"${dimAttr} decoding="async" loading="${load}"`);
}
{
const { url } = emit(join(ROOT, "assets/dist/delivery-flow.svg"), "delivery-flow", "svg");
body = body.replace(`src="__DIAG_FLOW__"`,
`src="${url}" width="1180" height="660" decoding="async" loading="lazy"`);
}
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").url;
const stRef = emit(join(ROOT, "st.min.js"), "st.min", "js").url;
const appName = `arc.${fp(app)}.js`;
writeFileSync(join(DIST, "assets", appName), app);
const appRef = "/assets/" + appName;
const DESC = "Interactive teardown of Kubernetes and OpenShift delivery: containers, Services, GitOps, image supply chains and Helm, explained with scroll-driven exploded diagrams.";
const OG_IMG = SITE + heroUrls["__HERO_GITOPS__"];
// shared document shell: every page on the site gets the same head discipline
const shell = ({ title, desc, path, ogImg, scripts, bodyHtml }) => `<!doctype html>
<html lang="en-AU">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title}</title>
<meta name="description" content="${desc}">
<meta name="theme-color" content="#070b14">
<meta name="build-revision" content="${BUILD_REV}">
<link rel="canonical" href="${SITE}${path}">
<meta property="og:title" content="${title}">
<meta property="og:description" content="${desc}">
<meta property="og:type" content="website">
<meta property="og:url" content="${SITE}${path}">
<meta property="og:image" content="${ogImg}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${title}">
<meta name="twitter:description" content="${desc}">
<meta name="twitter:image" content="${ogImg}">
<link rel="icon" href="${favicon}">
${SKIP_CSS}
${MAST_CSS}
</head>
<body>
<a class="skip" href="#main-content">Skip to content</a>
<main id="main-content">
${bodyHtml}
</main>
${scripts.map(s => `<script src="${s}"></script>`).join("\n")}
</body>
</html>
`;
const html = shell({
title: "The Exploded Cluster", desc: DESC, path: "/cluster", ogImg: OG_IMG,
scripts: [gsapRef, stRef, appRef], bodyHtml: body,
});
// ---- The Mirror (chapter 02): fingerprint its plates + its app script -------
const MIRROR_DESC = "How a zot pull-through registry behaves under Talos and containerd: upstream routing, digest preservation, garbage collection, authentication rollout, failure modes, and the lab evidence behind each decision.";
let mirror = readFileSync(join(ROOT, "pilot/mirror.tpl.html"), "utf8");
const mirrorPlates = {};
for (const n of ["20", "21", "22", "23", "24", "25"]) {
const { url, buf } = emit(join(ROOT, "assets/mirror", `${n}.jpg`), `mirror-${n}`, "jpg");
mirrorPlates[n] = url;
const dims = jpegSize(buf);
if (!dims) throw new Error(`mirror plate ${n}: JPEG dimensions unreadable`);
// every plate sits below the fold (fact panel + contents precede the first scene),
// so all six lazy-load; intrinsic dims come from the actual bytes.
mirror = mirror.replace(`__M${n}__`,
`${url}" width="${dims.w}" height="${dims.h}" decoding="async" loading="lazy`);
}
const mirrorJs = readFileSync(join(ROOT, "pilot/mirror.js"));
const mirrorJsName = `mirror.${fp(mirrorJs)}.js`;
writeFileSync(join(DIST, "assets", mirrorJsName), mirrorJs);
mirror = mirror.replace("__MIRROR_JS__", "/assets/" + mirrorJsName);
if (mirror.includes("__M")) throw new Error("unfilled mirror placeholder");
const mirrorHtml = shell({
title: "The Mirror", desc: MIRROR_DESC, path: "/mirror",
ogImg: SITE + mirrorPlates["20"], scripts: [], bodyHtml: mirror,
});
// ---- the directory (index): the chapters of what this place teaches ---------
const DIR_DESC = "The Teaching Lab: scroll-driven teardowns of real infrastructure - container platforms, registries and the machinery of delivery, one exploded system per chapter.";
let dir = readFileSync(join(ROOT, "pilot/index.tpl.html"), "utf8")
.replace("__THUMB_CLUSTER__", heroUrls["__HERO_GITOPS__"])
.replace("__THUMB_MIRROR__", mirrorPlates["20"]);
if (dir.includes("__THUMB")) throw new Error("unfilled directory placeholder");
const dirHtml = shell({
title: "The Teaching Lab", desc: DIR_DESC, path: "/",
ogImg: SITE + mirrorPlates["20"], scripts: [], bodyHtml: dir,
});
writeFileSync(join(DIST, "index.html"), dirHtml);
writeFileSync(join(DIST, "cluster.html"), html);
writeFileSync(join(DIST, "mirror.html"), mirrorHtml);
writeFileSync(join(DIST, "build.json"), JSON.stringify({ revision: BUILD_REV, app: appName, mirror: mirrorJsName }) + "\n");
// ---------------------------------------------------------------------------
// Preflight gates: fail the build loudly rather than ship a known defect.
// ---------------------------------------------------------------------------
const fail = [];
const must = (cond, msg) => { if (!cond) fail.push(msg); };
// per-page structural gates (CSP + document hygiene) for every page shipped
for (const [pg, doc] of [["cluster", html], ["mirror", mirrorHtml], ["index", dirHtml]]) {
must(!(doc.match(/<script(?![^>]*src=)[^>]*>/gi) || []).length, `${pg}: inline <script> present`);
must(!/__[A-Z0-9_]+__/.test(doc), `${pg}: unresolved __PLACEHOLDER__ in output`);
must((doc.match(/<h1[\s>]/gi) || []).length === 1, `${pg}: expected exactly one <h1>`);
const pids = [...doc.matchAll(/\sid="([^"]+)"/g)].map(m => m[1]);
must(new Set(pids).size === pids.length, `${pg}: duplicate id attribute(s): ` +
pids.filter((v, k) => pids.indexOf(v) !== k).join(", "));
const pidset = new Set(pids);
for (const m of doc.matchAll(/href="#([^"]+)"/g)) {
if (m[1] && !pidset.has(m[1])) fail.push(`${pg}: dangling internal link #` + m[1]);
}
}
// mirror-page content gates: retired claims may be DISCUSSED as corrections, so these
// target the assertive phrasings and internal production jargon, not the topic words.
const mirrorBanned = [
[/never leaves the building/i, "retired absolute cache claim"],
[/SAY IT/, "removed rhetorical block resurfaced"],
[/HOMELAB CLAUSE/, "removed label resurfaced"],
[/Nothing on v2\.1\.17\/18 protects/, "over-broad retention annotation resurfaced"],
[/100 manifest requests per six hours/, "imprecise Hub limit phrasing"],
[/a tag is a suggestion, a digest is a fact/i, "retired slogan"],
[/up to twelve|twelve tries|12 attempts/i, "stale retry arithmetic (correct figure: sixteen)"],
[/plate-level/i, "internal production note leaked to readers"],
[/&middot; motion:/i, "art-direction caption leaked to readers"],
[/Phase B/, "internal phase jargon without explanation"],
[/RAID .*(is|are) (a )?backup/i, "snapshots/RAID-as-backup claim"],
];
for (const [re, msg] of mirrorBanned) if (re.test(mirrorHtml)) fail.push("mirror: " + msg);
const mirrorRequired = [
["zotregistry.dev", "primary zot references missing"],
["docs.docker.com/docker-hub/usage/pulls", "Docker Hub pulls reference missing"],
["v1alpha1", "Talos auth-reboot source link missing"],
["deleteUntagged", "blanket untagged lever discussion missing"],
["keepUntagged", "version-gated retention discussion missing"],
["/livez", "real health endpoints missing"],
['id="refs"', "references section missing"],
['id="open"', "open-verification register missing"],
['width="1600" height="872"', "plate intrinsic dimensions missing/incorrect"],
];
for (const [needle, msg] of mirrorRequired) if (!mirrorHtml.includes(needle)) fail.push("mirror: " + msg);
must((mirrorHtml.match(/width="1600" height="872"/g) || []).length === 6, "mirror: expected six correctly-dimensioned plates");
must((mirrorHtml.match(/loading="lazy"/g) || []).length === 6, "mirror: expected all six plates lazy-loaded");
must((mirrorHtml.match(/<caption/g) || []).length === 5, "mirror: expected five table captions");
must(!/<th>(?!<)/.test(mirrorHtml.replace(/<th scope="col">/g, "")), "mirror: th without scope=col");
must(!/100vw/.test(mirrorHtml), "mirror: viewport-width unit reintroduces scrollbar overflow");
must(!mirrorHtml.includes("gsap"), "mirror: unexpected animation library reference");
// banned / obsolete strings (user-visible copy)
const banned = [
[/ArgoCD/, "user-visible 'ArgoCD' (use 'Argo CD')"],
[/random non-root UID/, "obsolete 'random non-root UID'"],
[/nothing anywhere records that it moved/, "obsolete tag-immutability claim"],
[/no interface owns that address/, "obsolete universal ClusterIP claim"],
[/Three commands/, "'Three commands' contradicts four shown"],
[/selfHeal puts it back; what leaves git leaves the cluster/, "stale unconditional self-heal legend"],
[/every manifest <span class="mono">include<\/span>s/, "broken 'include s' construction"],
[/two policies renders from one field/, "subject/verb error 'renders from one field'"],
[/grown-up chart/, "'grown-up chart' (use 'production-grade chart')"],
[/journalctl -u kubelet/, "node-shell instruction (use oc describe / oc debug node)"],
];
// legend blurbs are inlined into the app JS (the SCENES const), so scan both surfaces
const visible = html + "\n" + app;
for (const [re, msg] of banned) if (re.test(visible)) fail.push(msg);
if (/\bnever by SSH\b/.test(visible)) fail.push("obsolete 'never by SSH'");
if (/random non-root UID/.test(visible)) fail.push("obsolete 'random non-root UID' (also check legends)");
if (fail.length) { console.error("PREFLIGHT FAILED:\n - " + fail.join("\n - ")); process.exit(1); }
console.log(`site build -> ${DIST}: index.html ${(html.length / 1024).toFixed(0)}KB, rev ${BUILD_REV}, preflight OK (${appName})`);