Put a content hash in the asset filenames - #17
Merged
Merged
Conversation
The CSS and JS shipped under fixed names, so a deploy changed what /style/main.css contained without changing its URL. A browser holding the old copy has no reason to ask for it again, and these come back as max-age=14400, so for four hours after every deploy a returning visitor gets fresh HTML wired to stale CSS and JS. That is not an edge case, it is everyone who visited in the four hours before the deploy. It cost us a whole debugging round. The Safari mosaic fix went out, the site was correct in a private tab, and an iPhone that had been on the site earlier the same day kept showing the old broken build. Two of my diagnoses were of a bug that was no longer in the code. build:fingerprint renames each built asset to carry a hash of its own bytes and repoints the emitted HTML at the new name, so a URL's contents can never change: a deploy publishes a new name, and the HTML, which revalidates every load, points at it. Six assets, whose only references are the four templates that link them. It hashes the built output rather than the source, so a change that esbuild optimises away does not needlessly bust anyone's cache, and it fails the build rather than shipping a reference it could not rewrite. Only in `npm run build`. Watch mode rebuilds these continuously and the templates link them unhashed, so dev keeps the plain names. _headers now asks for these to be kept a year rather than revalidated every load, which hashed names make safe. Worth knowing that Cloudflare has been ignoring Cache-Control from that file: these came back as max-age=14400 while the rule said max-age=0, so the override is likely a Browser Cache TTL in the dashboard. The hashed names are what actually fixes this; the header only decides how long the correct file is kept. Claude-Session: https://claude.ai/code/session_013oNA6ETp6gzSGmmpyn3euT
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.
Fixes the stale-asset window that made the Safari mosaic fix look like it had not shipped.
The problem
/style/main.cssand/scripts/main.jshad fixed names, so a deploy changed their contents without changing their URLs. The HTML revalidates every request (max-age=0), but the assets come back asmax-age=14400, andmust-revalidateonly applies once an entry is already stale. So for four hours after any deploy, anyone who had visited in the four hours before it gets fresh HTML pointing at stale CSS and JS.This is what happened after #16. The fix was live and correct, desktop Safari was fine, a private tab was fine, and an iPhone that had loaded the site earlier the same day kept rendering the old build. I diagnosed a WebKit sizing bug twice against code that was no longer running.
The fix
scripts/fingerprint-assets.mjs, wired in asbuild:fingerprintat the end ofnpm run build. It renames each built asset to carry a hash of its own bytes and repoints the emitted HTML:A URL's contents can now never change, so a stale copy is never the one the page asks for. Six assets, each referenced from exactly one template, nothing off-site linking to them.
Three deliberate choices:
npm run devkeeps the plain names. Verified:build:sitealone still emits/style/main.css.It also clears previously-hashed copies before writing, so repeated local builds do not pile up in
dist.Cache headers
_headersnow asks for a year andimmutableinstead of revalidate-every-load, which hashed names make safe, and which caches strictly better than today.Worth knowing separately: Cloudflare appears to be ignoring
Cache-Controlfrom that file. The old rule said/scripts/* max-age=0and the live response wasmax-age=14400. That override is most likely a Browser Cache TTL set in the Cloudflare dashboard, which is outside this repo. It does not block this PR, since the hashed names are what actually make staleness impossible and the header only decides how long the correct file is kept, but it is worth a look at the dashboard.Verification
diststyle/main.csssrc/index.tsso the output changedLoaded
/,/pricing.html,/link.htmland/manifesto.htmlin WebKit and Chromium, at iPhone 14 and 1440x900: no 404s, no JS errors, stylesheet applied, and the mosaic still builds both tracks at 1920px and 10.7 px/sec on mobile, 3078px and 17.1 px/sec on desktop.