Skip to content

Version static asset URLs by content hash - #43

Merged
botre merged 1 commit into
masterfrom
fix/fingerprint-static-assets
Aug 8, 2026
Merged

Version static asset URLs by content hash#43
botre merged 1 commit into
masterfrom
fix/fingerprint-static-assets

Conversation

@botre

@botre botre commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Production is currently rendering unstyled. This fixes it and stops it recurring.

What happened

Replacing the Tailwind CDN with a first-party stylesheet put the CSS at a fixed URL, /app.css, with no fingerprint. When the colour rework shipped, Cloudflare kept serving the previous stylesheet for its full max-age=14400 lifetime while the origin served the new HTML.

Measured on production while diagnosing:

/app.css  →  31,480 bytes, contains .text-slate-500 and .bg-indigo-600
             cf-cache-status: HIT, age: 4808, cache-control: max-age=14400

That is the pre-rework vocabulary. The new markup asks for text-neutral-500, bg-brand-600, bg-get-wash — none of which exist in the cached file — so every colour resolves to nothing. Borders come out black because border-color falls back to currentColor when its rule is missing, which is why it looked like a rendering bug rather than a caching one.

The HTML itself is cf-cache-status: DYNAMIC, so deploying this fixes production immediately: the new markup points at a URL the edge has never seen. No purge needed.

The fix

Every first-party asset the templates reference carries a short content hash in its URL:

/app.css?v=21de7008dc
/index.js?v=3180e57ef8   /render-body.js?v=f0a4431e4d
/har.js?v=6634e21ce3     /endpoint.js?v=c140a7e6dc

A changed file is a different URL, so no cache can pair it with the wrong HTML. Images are excluded: they are replaced rarely and never in a way that breaks a page holding the previous copy.

Hashes are computed once at startup. Development re-hashes on demand so an edited stylesheet needs no restart.

One thing worth flagging

The first implementation re-hashed all five files on every render in development. That made template rendering the bottleneck under parallel load: the e2e suite went from 4 seconds to 32 and started failing tests intermittently. I nearly wrote that off as flake — it reproduced as "first run after a server start is slow and loses 3-4 tests, subsequent runs are green".

Keying the dev re-hash on modification time fixed it: 3.5s and 39/39 on the cold run, three consecutive clean full runs.

Template strings use backquotes rather than nested double quotes, so the layout stays parseable as HTML — Prettier caught that the first form was malformed even though Go's template engine handled it.

Verification

go vet, gofmt, go build, unit tests, prettier --check . clean. Playwright 39/39 across three consecutive runs. Verified the rendered URLs resolve and that the hash tracks file contents.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X8JeP7AQwpd2coSfwbMopt

The stylesheet and the page scripts are served from fixed paths. Replacing the
Tailwind CDN with a first-party stylesheet made that a deployment hazard: a CDN
or a browser holding a cached copy from one deploy pairs it with the HTML of the
next, every class name the new markup asks for resolves to nothing, and the page
renders unstyled. Border colours fall back to currentColor, so it arrives as
black boxes rather than as anything that looks like a caching problem.

That is not hypothetical. The colour rework shipped and the edge served the
previous stylesheet for its full four-hour lifetime against the new markup.

Each versioned asset now carries a short hash of its contents in the query
string, so a changed file is a different URL and no cache can confuse it for the
old one. Hashes are computed once at startup. Development re-hashes on demand so
an edited stylesheet needs no restart, but only when the file's modification time
has moved: hashing five files on every render made template rendering the
bottleneck under parallel load and turned a four-second test suite into
thirty-two.

Template strings use backquotes so the attribute's own quotes are not nested and
the file stays parseable as HTML.

Claude-Session: https://claude.ai/code/session_01X8JeP7AQwpd2coSfwbMopt
@botre
botre merged commit 8327a37 into master Aug 8, 2026
3 checks passed
@botre
botre deleted the fix/fingerprint-static-assets branch August 8, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant