fix(pages): restore the cache semantics four assets lost - #1200
Conversation
public/_headers promises to replicate what the previous CDN served, but four assets fall through it. robots.txt, favicon.ico and logo.png have no rule at all and take Cloudflare's default instead of the values the upload step used to set. The WebAssembly chunk of the main build lands in the build root, because config-overrides.js set webassemblyModuleFilename only in the branch guarded by CUSTOM_CHUNK_PATH, which only the widget build sets; it is served with max-age=0 today, where the previous step served it as immutable. That chunk is content-hashed, so it belongs with the other fingerprinted assets: the default output path now points at static/wasm/, which the existing /static/* rule already covers, and the widget build keeps overriding it with its own versioned path. A /*.module.wasm rule would work as well, since Cloudflare does support splat patterns of that shape, but letting one rule keep covering the whole fingerprinted bucket is the smaller change. robots.txt also gets its own comment, so the reason it must not be cached is visible in the file itself rather than only in the pull request.
The comment above the robots.txt rule said "do not cache", but the value it describes is no-cache, must-revalidate. That does not forbid storing the response; it forbids reusing it without a successful revalidation, and only no-store forbids storing. The entry-point comment a few lines above already words it that way, so the two now match. The header value itself is unchanged.
|
Six review passes over two lenses — project conformity and logic/correctness — until both came back with zero findings. They were worth it: the passes are what turned up the wasm chunk in the first place, and then three wrong statements of mine about it. What changed along the way:
Both build paths were run rather than reasoned about: One practical note for merging. If this is squashed, GitHub prefills the message with both commit messages, and the first one still contains the imprecise "must not be cached" wording described at the end of the body. It cannot be corrected in place without rewriting a pushed commit. A message that reflects the final state: |
|
Measured after the deploy. Three of the four assets are exactly as intended, one is not:
So the wasm chunk, which was the point of this PR, is served as immutable now, and the two root assets carry the day of cache they are supposed to.
A cache-busting query string returns the same value, so this is not a stale edge copy. The fix is to state the same intent with an explicit number: The table in the description above is corrected accordingly; the |
Follow-up to #1198. That PR removed the Azure deploy branch; this one closes the gaps it made visible.
public/_headersopens with a promise:Four assets break it. Measured against the deployed site rather than assumed:
/robots.txtno-cache, must-revalidatepublic, max-age=14400, must-revalidate/favicon.icopublic, max-age=86400public, max-age=14400, must-revalidatepublic, max-age=86400/logo.pngpublic, max-age=86400public, max-age=14400, must-revalidatepublic, max-age=86400public, max-age=31536000, immutablepublic, max-age=0, must-revalidatepublic, max-age=31536000, immutable/manifest.jsonno-cache, must-revalidateno-cache, must-revalidateThe previous values are the ones the removed upload step set:
index.html,manifest.json,asset-manifest.jsonandrobots.txtshared oneno-cacheloop,*.ico/*.pngwere uploaded with a day of cache, and*.wasmwas uploaded as immutable through a pattern that matched the whole build tree.The wasm chunk
This is the one worth reading closely. Before this PR,
config-overrides.jssetwebassemblyModuleFilenameonly inside the branch guarded byCUSTOM_CHUNK_PATH, and onlyscripts/build-widget.shsets that variable. The main build therefore fell back to the webpack default and wrote the chunk into the build root:asset-manifest.jsonon the deployed site lists"module.wasm": "/c2d852c5a3680f3096b5.module.wasm", and that URL answers withcache-control: public, max-age=0, must-revalidate— roughly 700 KB of hardware-wallet driver revalidating on every use, where the previous CDN served it as immutable.The filename is content-hashed, so the file belongs where every other fingerprinted asset already lives. This PR sets the default output path to
static/wasm/, which the existing/static/*rule covers, and the widget branch keeps overriding it withv<version>-chunks/as before.Moving the file rather than adding a
/*.module.wasmrule is a judgement call, not a technical necessity — Cloudflare does support splat patterns of that shape. The reason to prefer the move: the file is fingerprinted, so it belongs in the bucket the existing rule already describes, and one rule keeps covering it instead of a second rule existing for a single asset.Verified with a real build rather than argued:
npm run build:devnow emitsbuild/static/wasm/c2d852c5a3680f3096b5.module.wasm— same hash as the file live today, so same content in a covered location — and leaves no.wasmin the build root.npm run widget:devstill emitswidget/v1.0-chunks/<hash>.module.wasm, which is the path theCopy widgetstep in both pipelines copies from. That/static/*matches across path segments is not an assumption either:/static/js/main.6f300bae.jsanswers withpublic, max-age=31536000, immutableon the deployed site.The three root files
robots.txtmatters in practice: a change to crawler directives should take effect on the next request. It now carries its own comment so that reason is visible in the file rather than only in this description.favicon.icoandlogo.pngare restored to the day of cache the previous CDN gave them. Neither is fingerprinted —public/index.htmlreferences both by plain path — so this is a trade rather than a pure win: a replaced logo can sit in a browser cache for a day instead of four hours. That was the behaviour before the migration and the file claims that value, so it should hold it. If the shorter window is preferred, the right fix is to say so in_headersexplicitly rather than to leave the entry missing.What is deliberately left without a rule
version.jsonis written topublic/version.jsonat build time byscripts/generate-version.jsand is gitignored, so it never shows up in the committed tree — but it reaches the deployed root exactly like the other files inpublic/. It still gets no rule, because it does not need one: Cloudflare's default for it is alreadypublic, max-age=0, must-revalidate, which is what a version marker wants. With that, every file the deployed root contains is either covered by a rule or correct by default./manifest.jsonis the evidence that exact paths take effect at all: it has an exact rule and the deployed site returns exactly that value.Scope
public/_headersandconfig-overrides.js. Existing rules keep their values and their order; the widget build path is untouched.One correction that lives in the history
The first commit's message says the new comment makes visible "the reason it must not be cached". That is imprecise, in the same way the comment itself was:
no-cache, must-revalidatedoes not forbid storing the response, it forbids reusing it without a successful revalidation — onlyno-storeforbids storing. The second commit corrects the comment in the file and spells out the distinction, but the first commit's own prose cannot be corrected without rewriting a pushed commit, so it is flagged here instead. On a squash merge, the second commit's wording is the accurate one.