From 3edd34de0dfe118517a00b0c48deda4d34412bd1 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 3 Nov 2026 08:24:01 +0000 Subject: [PATCH] fix(auctions): version the stylesheet URL so the theme switch never hits stale CSS Co-Authored-By: Claude Opus 5.5 --- src/auctions.ts | 11 ++++++++--- test/auctions.test.ts | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/auctions.ts b/src/auctions.ts index 24f38dd..0c867d2 100644 --- a/src/auctions.ts +++ b/src/auctions.ts @@ -1,7 +1,7 @@ import { Database } from 'bun:sqlite' import { mkdirSync } from 'node:fs' import { dirname } from 'node:path' -import { randomBytes } from 'node:crypto' +import { createHash, randomBytes } from 'node:crypto' import { Hono } from 'hono' import { RateLimiter } from './ratelimit.js' @@ -62,9 +62,14 @@ function validImage(type: string, bytes: Uint8Array): boolean { return false } +// The stylesheet URL carries a hash of its contents, so a browser never keeps an old copy +// after a deploy (a cached pre-theme stylesheet left the Blue switch doing nothing). +let cssVersion: string | undefined +const cssHref = () => `/style.css?v=${(cssVersion ??= createHash('sha256').update(AUCTIONS_CSS).digest('hex').slice(0, 12))}` + function page(theme: AuctionTheme, title: string, body: string, description = 'A home for art, artists and the stories behind every piece.', canonicalPath = '/') { return ` -${esc(title)} · Art Auctions +${esc(title)} · Art Auctions
AArt AuctionsTHE HOUSE FOR ORIGINAL WORK
${body}` } @@ -153,7 +158,7 @@ export function createAuctionApp(house: AuctionHouse) { c.header('set-cookie', `${THEME_COOKIE}=${name}; Path=/; Max-Age=31536000; SameSite=Lax; Secure`) return c.redirect(target, 303) }) - app.get('/style.css', (c) => c.body(AUCTIONS_CSS, 200, { 'content-type': 'text/css; charset=utf-8', 'cache-control': 'public, max-age=3600' })) + app.get('/style.css', (c) => c.body(AUCTIONS_CSS, 200, { 'content-type': 'text/css; charset=utf-8', 'cache-control': c.req.query('v') ? 'public, max-age=31536000, immutable' : 'no-cache' })) app.get('/image/:id', (c) => { const image = house.image(c.req.param('id')) if (!image) return c.text('Not found', 404) diff --git a/test/auctions.test.ts b/test/auctions.test.ts index 4826f72..0cd7ccd 100644 --- a/test/auctions.test.ts +++ b/test/auctions.test.ts @@ -90,7 +90,11 @@ describe('Art Auctions House', () => { expect(warm).toContain('data-theme="blue"') expect(warm).toContain('content="#eef3fa"') - const css = await (await app.fetch(new Request('https://auctions.pit.world/style.css'))).text() + const href = /