Version static asset URLs so releases bust the CSS/JS cache

Cached app.css from before 0.4.0 left the score keeper page unstyled on phones.
Assets are now /static/x?v=<version> with a one-year immutable cache; unversioned
requests get 60s. Release 0.4.2.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi
This commit is contained in:
2026-09-04 02:41:15 +00:00
co-authored by Claude Fable 5.1
parent ae44014e22
commit a5222953dd
5 changed files with 15 additions and 6 deletions
+4 -2
View File
@@ -48,14 +48,16 @@ export const send = {
};
const MIME = { '.css': 'text/css', '.js': 'text/javascript', '.png': 'image/png', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json', '.json': 'application/json' };
export async function serveStatic(res, root, urlPath) {
export async function serveStatic(res, root, urlPath, versioned = false) {
const p = normalize(join(root, urlPath));
if (!p.startsWith(root)) throw new HttpError(404, 'Not found');
try {
const s = await stat(p);
if (!s.isFile()) throw new HttpError(404, 'Not found');
const body = await readFile(p);
res.writeHead(200, { 'content-type': MIME[extname(p)] ?? 'application/octet-stream', 'cache-control': 'public, max-age=300' });
// Assets are referenced with ?v=<version>, so a new release is a new URL: cache the versioned
// URL for a year, and keep unversioned requests short-lived.
res.writeHead(200, { 'content-type': MIME[extname(p)] ?? 'application/octet-stream', 'cache-control': versioned ? 'public, max-age=31536000, immutable' : 'public, max-age=60, must-revalidate' });
res.end(body);
} catch (e) { if (e instanceof HttpError) throw e; throw new HttpError(404, 'Not found'); }
}
+1 -1
View File
@@ -25,7 +25,7 @@ const staticRoot = join(here, 'public');
const server = createServer(async (req, res) => {
const url = new URL(req.url, 'http://x');
try {
if (url.pathname.startsWith('/static/')) return await serveStatic(res, staticRoot, url.pathname.slice('/static'.length));
if (url.pathname.startsWith('/static/')) return await serveStatic(res, staticRoot, url.pathname.slice('/static'.length), url.searchParams.has('v'));
const m = router.match(req.method, url.pathname);
if (!m) throw new HttpError(404, 'Not found');
await m.handler(req, res, m.params);
+2 -2
View File
@@ -13,7 +13,7 @@ export function layout({ title, body, state = null, script = null, bodyClass = '
<link rel="manifest" href="/static/manifest.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Source+Sans+3:wght@400;600&family=JetBrains+Mono:wght@500&display=swap">
<link rel="stylesheet" href="/static/app.css">
<link rel="stylesheet" href="/static/app.css?v=${VERSION}">
</head>
<body class="${esc(bodyClass)}">
<header class="top">
@@ -25,7 +25,7 @@ ${body}
</main>
<footer class="foot">Courtside v${VERSION} · self-hosted tournament desk · <a href="https://gitea.cloudfreeiot.com/bkvargyas/courtside">source</a> · <a href="/admin">organizer desk</a></footer>
${state ? `<script id="state" type="application/json">${JSON.stringify(state).replace(/</g, '\\u003c')}</script>` : ''}
${script ? `<script src="${esc(script)}" defer></script>` : ''}
${script ? `<script src="${esc(script)}?v=${VERSION}" defer></script>` : ''}
</body>
</html>`;
}