pre-review hardening: technical qualifiers across all courses, git/Argo CD proper nouns, a11y (skip link, main, focus-visible, reduced-motion net, textContent legends), perf (intrinsic image dims, decoding async, lazy scenes + will-change lifecycle), nginx ^~ /assets/ priority, OG/canonical/build-revision, preflight gates
This commit is contained in:
+102
-12
@@ -1,7 +1,8 @@
|
||||
#!/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 arc.js, injects the scene
|
||||
// manifests, points heroes at /assets/ files, and wraps a full HTML document.
|
||||
// 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";
|
||||
@@ -10,9 +11,35 @@ 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;
|
||||
}
|
||||
|
||||
let tpl = readFileSync(join(ROOT, "pilot/arc.tpl.html"), "utf8");
|
||||
|
||||
// split the app script (the last <script> block) out of the template
|
||||
@@ -53,36 +80,65 @@ const emit = (srcPath, base, ext) => {
|
||||
const buf = readFileSync(srcPath);
|
||||
const name = `${base}.${fp(buf)}.${ext}`;
|
||||
writeFileSync(join(DIST, "assets", name), buf);
|
||||
return "/assets/" + name;
|
||||
return { url: "/assets/" + name, buf };
|
||||
};
|
||||
|
||||
const heroUrls = {};
|
||||
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"));
|
||||
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}"` : "";
|
||||
// Course 00's hero is the first visible image -> load eager; the rest are below the fold.
|
||||
const load = ph === "__HERO_SURROUND__" ? "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"`);
|
||||
}
|
||||
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 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__"];
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>The Exploded Cluster</title>
|
||||
<meta name="description" content="Interactive teardown of the toolchain around Kubernetes and OpenShift: GitOps, the image supply chain, and Helm - every scene one generated hero, sliced and scrubbed.">
|
||||
<meta name="description" content="${DESC}">
|
||||
<meta name="theme-color" content="#070b14">
|
||||
<meta name="build-revision" content="${BUILD_REV}">
|
||||
<link rel="canonical" href="${SITE}/">
|
||||
<meta property="og:title" content="The Exploded Cluster">
|
||||
<meta property="og:description" content="${DESC}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="${SITE}/">
|
||||
<meta property="og:image" content="${OG_IMG}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="The Exploded Cluster">
|
||||
<meta name="twitter:description" content="${DESC}">
|
||||
<meta name="twitter:image" content="${OG_IMG}">
|
||||
<link rel="icon" href="${favicon}">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#main-content">Skip to content</a>
|
||||
<main id="main-content">
|
||||
${body}
|
||||
</main>
|
||||
<script src="${gsapRef}"></script>
|
||||
<script src="${stRef}"></script>
|
||||
<script src="${appRef}"></script>
|
||||
@@ -91,8 +147,42 @@ ${body}
|
||||
`;
|
||||
|
||||
writeFileSync(join(DIST, "index.html"), html);
|
||||
writeFileSync(join(DIST, "build.json"), JSON.stringify({ revision: BUILD_REV, app: appName }) + "\n");
|
||||
|
||||
// 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, all assets fingerprinted (${appName})`);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preflight gates: fail the build loudly rather than ship a known defect.
|
||||
// ---------------------------------------------------------------------------
|
||||
const fail = [];
|
||||
const must = (cond, msg) => { if (!cond) fail.push(msg); };
|
||||
|
||||
// no inline <script> may survive (CSP script-src 'self')
|
||||
must(!(html.match(/<script(?![^>]*src=)[^>]*>/gi) || []).length, "inline <script> present");
|
||||
// no unresolved template placeholders
|
||||
must(!/__[A-Z_]+__/.test(html), "unresolved __PLACEHOLDER__ in output");
|
||||
// exactly one <h1>
|
||||
must((html.match(/<h1[\s>]/gi) || []).length === 1, "expected exactly one <h1>");
|
||||
// unique ids
|
||||
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map(m => m[1]);
|
||||
must(new Set(ids).size === ids.length, "duplicate id attribute(s): " +
|
||||
ids.filter((v, k) => ids.indexOf(v) !== k).join(", "));
|
||||
// internal anchor targets resolve
|
||||
const idset = new Set(ids);
|
||||
for (const m of html.matchAll(/href="#([^"]+)"/g)) {
|
||||
if (m[1] && !idset.has(m[1])) fail.push("dangling internal link #" + m[1]);
|
||||
}
|
||||
// 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')"],
|
||||
];
|
||||
for (const [re, msg] of banned) if (re.test(html)) fail.push(msg);
|
||||
|
||||
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})`);
|
||||
|
||||
Reference in New Issue
Block a user