index + purpose sections, full asset fingerprinting, credit footer
This commit is contained in:
+47
-2
@@ -7,6 +7,31 @@
|
||||
<p class="scrollcue" aria-hidden="true">scroll<span></span></p>
|
||||
</header>
|
||||
|
||||
<nav class="toc" aria-label="index">
|
||||
<p class="kt">Index</p>
|
||||
<ol>
|
||||
<li><a href="#c1"><b>Course I</b><span>Docker - an image is a stack of frozen diffs</span></a></li>
|
||||
<li><a href="#c2"><b>Course II</b><span>Kubernetes - a cluster is a promise, not a place</span></a></li>
|
||||
<li><a href="#c6"><b>Course VI</b><span>GitOps - nobody deploys anything; the cluster syncs itself</span></a></li>
|
||||
<li><a href="#c7"><b>Course VII</b><span>The image supply chain - a tag is a promise, a digest is a fact</span></a></li>
|
||||
<li><a href="#c8"><b>Course VIII</b><span>Helm - a chart is a function; the values file is the cluster speaking</span></a></li>
|
||||
<li><a href="#apx"><b>Appendix</b><span>The dependency ledger - GitHub, Azure Key Vault, zot</span></a></li>
|
||||
<li><a href="#flow"><b>Blueprint</b><span>The delivery flow, end to end</span></a></li>
|
||||
<li><a href="#library"><b>Library</b><span>Official documentation for every technology on this page</span></a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<section class="purpose" aria-labelledby="purph">
|
||||
<p class="kt" id="purph">Purpose of this document</p>
|
||||
<p>The Exploded Cluster teaches how modern container platforms work by taking them apart -
|
||||
literally. Each course is one machine drawn as a single exploded illustration, sliced into its
|
||||
real components and wired to your scroll, so the architecture moves while the words explain it.
|
||||
Start at Course I with what an image actually is; finish knowing how a change travels from a
|
||||
git commit to a running, secret-fed, digest-pinned workload on a fleet. Every claim comes from
|
||||
running these systems for real, and the library at the end links only to official
|
||||
documentation.</p>
|
||||
</section>
|
||||
|
||||
<aside class="key" aria-label="reading key">
|
||||
<p class="kt">Reading key · this page stands alone - anything it leans on is listed here</p>
|
||||
<ul>
|
||||
@@ -413,8 +438,10 @@
|
||||
</section>
|
||||
|
||||
<footer class="outro">
|
||||
<p>The Exploded Cluster · the delivery arc · every scene is one generated hero,
|
||||
sliced and scrubbed - no diagrams were harmed.</p>
|
||||
<p>The Exploded Cluster · every scene is one generated hero, sliced and scrubbed -
|
||||
no diagrams were harmed.</p>
|
||||
<p>© 2026 Jonathon Wright · <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>
|
||||
· built with GSAP, shipped via GitOps · <a href="https://www.bztmon.com/">bztmon.com</a></p>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
@@ -442,6 +469,24 @@ b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cya
|
||||
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)}
|
||||
|
||||
.toc{max-width:760px;margin:0 auto;padding:0 24px 5vh}
|
||||
.toc .kt,.purpose .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;
|
||||
border-bottom-width:1px;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}}
|
||||
|
||||
.purpose{max-width:66ch;margin:0 auto;padding:0 24px 5vh}
|
||||
.purpose p:not(.kt){font-size:.95rem;line-height:1.75;color:var(--dim);margin:0;text-align:left}
|
||||
|
||||
.key{max-width:760px;margin:0 auto;padding:0 24px 4vh}
|
||||
.key .kt{font-family:var(--mono);font-size:.68rem;letter-spacing:.16em;text-transform:uppercase;
|
||||
color:var(--dim);text-align:center;margin:0 0 1rem}
|
||||
|
||||
+27
-13
@@ -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})`);
|
||||
|
||||
Reference in New Issue
Block a user