fix(web): serve the public JSON data files no-cache, not immutable

The static plugin gave every file under public/ a year of `immutable`,
including the JSON the page fetches at runtime: whats-new.json (rewritten
every release), the new security-notices.json (edited by hand) and
manifest.json. None of them is content-hashed or `?v=`-busted, so a
browser could keep last release's copy, and a security notice added in a
later release might never reach it. A `.json` file now revalidates
(`no-cache`); every other static asset keeps its immutable year, and a
route that set its own Cache-Control still wins.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 08:59:39 +02:00
1 parent 0ab71d61d1
commit ada2309721
2 files changed
+23 -2

No files matched your search

+13 -1
View File
@@ -11,7 +11,8 @@
*
* That contract is load-bearing: assets are served `immutable` for a year, and
* `index.html` must revalidate every time or a deploy leaves browsers on stale
* markup (see `cacheBustAssets` in server.ts). No HTML is left behind the static
* markup (see `cacheBustAssets` in server.ts). The JSON data files the page fetches at
* runtime (whats-new.json, security-notices.json) revalidate too. No HTML is left behind the static
* plugin (every page has its own route), so the HTML half is asserted on the route.
*
* These tests drive a REAL WebServer on purpose. Asserting against an inline
@@ -73,6 +74,17 @@ describe('static asset Cache-Control headers', () => {
expect(res.headers.get('cache-control')).toBe('no-cache, no-store');
});
it('makes the runtime JSON data files revalidate instead of pinning them for a year', async () => {
// whats-new.json is rewritten every release and security-notices.json by hand; neither is
// content-hashed or `?v=`-busted, so an `immutable` copy could outlive the release that
// replaced it, and a security notice added later would never reach that browser.
for (const file of ['security-notices.json', 'whats-new.json', 'manifest.json']) {
const res = await get(`${baseUrl}/${file}`);
expect(res.status, file).toBe(200);
expect(res.headers.get('cache-control'), file).toBe('no-cache');
}
});
it('serves the rendered index with no-cache too, via its own route', async () => {
// Different code path from the above, same contract: index.html is generated per
// request (cacheBustAssets stamps ?v= onto every asset ref), so caching it would