Version static asset URLs by content hash - #43
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 fullmax-age=14400lifetime while the origin served the new HTML.Measured on production while diagnosing:
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 becauseborder-colorfalls back tocurrentColorwhen 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:
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