From a5222953dddd5c13cc50d0cabca9d34c60e8997c Mon Sep 17 00:00:00 2001 From: Brian Vargyas Date: Fri, 4 Sep 2026 02:41:15 +0000 Subject: [PATCH] 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= with a one-year immutable cache; unversioned requests get 60s. Release 0.4.2. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MB7nCCAscYsb3zzkT6LHZi --- CHANGELOG.md | 7 +++++++ package.json | 2 +- server/http.js | 6 ++++-- server/index.js | 2 +- server/views/layout.js | 4 ++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e005a..45afd9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ so you can always tell what a deployment is actually serving. ## [Unreleased] +## [0.4.2] - 2026-09-03 + +### Fixed +- Stylesheet and scripts are referenced with `?v=`, so a new release can no longer be + served with a cached old `app.css` (which made the score keeper page render unstyled on + phones after 0.4.0). Versioned assets are cached for a year; unversioned for a minute. + ## [0.4.1] - 2026-09-03 ### Added diff --git a/package.json b/package.json index e60d638..9edc9a8 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"courtside","type":"module","version":"0.4.1","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}} \ No newline at end of file +{"name":"courtside","type":"module","version":"0.4.2","scripts":{"test":"node --test test/*.test.js","sim":"node simulate.js","start":"node --disable-warning=ExperimentalWarning server/index.js","dev":"node --disable-warning=ExperimentalWarning --watch server/index.js","demo":"node --disable-warning=ExperimentalWarning scripts/seed-demo.js","smoke":"bash test/smoke.sh"},"description":"Self-hosted volleyball tournament desk: one QR code from check-in to live scores","main":"server/index.js","directories":{"test":"test"},"keywords":[],"author":"","license":"MIT","dependencies":{"qrcode":"^1.5.4","ws":"^8.21.3"},"engines":{"node":">=22.13"},"repository":{"type":"git","url":"https://gitea.cloudfreeiot.com/bkvargyas/courtside.git"}} \ No newline at end of file diff --git a/server/http.js b/server/http.js index c799785..650c946 100644 --- a/server/http.js +++ b/server/http.js @@ -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=, 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'); } } diff --git a/server/index.js b/server/index.js index 56ecf91..6c7bf66 100644 --- a/server/index.js +++ b/server/index.js @@ -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); diff --git a/server/views/layout.js b/server/views/layout.js index b336cc5..e0f5a53 100644 --- a/server/views/layout.js +++ b/server/views/layout.js @@ -13,7 +13,7 @@ export function layout({ title, body, state = null, script = null, bodyClass = ' - +
@@ -25,7 +25,7 @@ ${body} ${state ? `` : ''} -${script ? `` : ''} +${script ? `` : ''} `; }