tangled
alpha
login
or
join now
zzstoatzz.io
/
zat
1
fork
atom
atproto tools in zig
zat.dev
sdk
atproto
zig
1
fork
atom
overview
issues
pulls
pipelines
docs: cache-bust assets and center footer
zzstoatzz.io
2 months ago
c2affa9e
453a7034
2/2
ci.yml
success
6s
deploy-docs.yml
success
7s
+50
-5
3 changed files
expand all
collapse all
unified
split
scripts
build-wisp-docs.mjs
site
app.js
style.css
+38
scripts/build-wisp-docs.mjs
···
8
8
access,
9
9
} from "node:fs/promises";
10
10
import path from "node:path";
11
11
+
import { execFile } from "node:child_process";
12
12
+
import { promisify } from "node:util";
11
13
12
14
const repoRoot = path.resolve(new URL("..", import.meta.url).pathname);
13
15
const docsDir = path.join(repoRoot, "docs");
14
16
const siteSrcDir = path.join(repoRoot, "site");
15
17
const outDir = path.join(repoRoot, "site-out");
16
18
const outDocsDir = path.join(outDir, "docs");
19
19
+
20
20
+
const execFileAsync = promisify(execFile);
17
21
18
22
async function exists(filePath) {
19
23
try {
···
53
57
return fallback.replace(/\.md$/i, "");
54
58
}
55
59
60
60
+
async function getBuildId() {
61
61
+
try {
62
62
+
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
63
63
+
cwd: repoRoot,
64
64
+
});
65
65
+
const full = String(stdout || "").trim();
66
66
+
if (full) return full.slice(0, 12);
67
67
+
} catch {
68
68
+
// ignore
69
69
+
}
70
70
+
return String(Date.now());
71
71
+
}
72
72
+
56
73
async function main() {
57
74
await rm(outDir, { recursive: true, force: true });
58
75
await mkdir(outDir, { recursive: true });
59
76
60
77
// Copy static site shell
61
78
await cp(siteSrcDir, outDir, { recursive: true });
79
79
+
80
80
+
// Cache-bust immutable assets on Wisp by appending a per-commit query string.
81
81
+
const buildId = await getBuildId();
82
82
+
const outIndex = path.join(outDir, "index.html");
83
83
+
if (await exists(outIndex)) {
84
84
+
let html = await readFile(outIndex, "utf8");
85
85
+
html = html.replaceAll('href="./style.css"', `href="./style.css?v=${buildId}"`);
86
86
+
html = html.replaceAll(
87
87
+
'src="./vendor/marked.min.js"',
88
88
+
`src="./vendor/marked.min.js?v=${buildId}"`,
89
89
+
);
90
90
+
html = html.replaceAll(
91
91
+
'src="./app.js"',
92
92
+
`src="./app.js?v=${buildId}"`,
93
93
+
);
94
94
+
html = html.replaceAll(
95
95
+
'href="./favicon.svg"',
96
96
+
`href="./favicon.svg?v=${buildId}"`,
97
97
+
);
98
98
+
await writeFile(outIndex, html, "utf8");
99
99
+
}
62
100
63
101
// Copy docs
64
102
await mkdir(outDocsDir, { recursive: true });
+10
-2
site/app.js
···
1
1
const navEl = document.getElementById("nav");
2
2
const contentEl = document.getElementById("content");
3
3
4
4
+
const buildId = new URL(import.meta.url).searchParams.get("v") || "";
5
5
+
6
6
+
function withBuild(url) {
7
7
+
if (!buildId) return url;
8
8
+
const sep = url.includes("?") ? "&" : "?";
9
9
+
return `${url}${sep}v=${encodeURIComponent(buildId)}`;
10
10
+
}
11
11
+
4
12
function escapeHtml(text) {
5
13
return text
6
14
.replaceAll("&", "&")
···
30
38
}
31
39
32
40
async function fetchJson(path) {
33
33
-
const res = await fetch(path, { cache: "no-cache" });
41
41
+
const res = await fetch(withBuild(path), { cache: "no-store" });
34
42
if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
35
43
return res.json();
36
44
}
37
45
38
46
async function fetchText(path) {
39
39
-
const res = await fetch(path, { cache: "no-cache" });
47
47
+
const res = await fetch(withBuild(path), { cache: "no-store" });
40
48
if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
41
49
return res.text();
42
50
}
+2
-3
site/style.css
···
163
163
164
164
.site-footer {
165
165
display: flex;
166
166
-
justify-content: flex-end;
166
166
+
justify-content: center;
167
167
padding: 12px 16px;
168
168
border-top: 1px solid var(--border);
169
169
-
background: color-mix(in srgb, var(--panel) 92%, transparent);
170
170
-
backdrop-filter: blur(10px);
169
169
+
background: var(--panel);
171
170
}
172
171
173
172
.footer-link {