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