Skip to content

Edge-cache plan

10 rules, one of them immutable

A static build is the easy case: fingerprinted assets can be cached forever and HTML cannot. What matters is writing down which is which, so the day a CMS makes pages editable the short window is already there instead of being discovered in a support ticket.

The rules this build serves

These are read from the same module next.config.ts imports, so the table and the headers cannot drift. They are checkable against the running site with a single curl per path.

Data table — data table
PathCache-ControlWhyReproduce
/:path*public, max-age=0, s-maxage=300, stale-while-revalidate=86400HTML: the browser revalidates every time and the edge holds it for five minutes, serving stale for up to a day while it refreshes. A correction ships within five minutes without stampeding the origin.curl -sI https://<host>/pricing | grep -i cache-control
/_next/static/:path*public, max-age=31536000, immutableEvery file under it carries a content hash in its name, so a changed file is a changed URL. Immutable is safe here and only here.curl -sI https://<host>/_next/static/chunks/<content-hashed-file>.js | grep -i cache-control
/_next/imagepublic, max-age=14400, must-revalidateThe optimizer sets this one itself (14400 is its default) and a config rule does not override it; the row is here because the table claims to list what the server sends. No page in this build uses it — there are 0 <img> tags.curl -sI https://<host>/_next/image?url=%2Fog%2Fdefault&w=64&q=75 | grep -i cache-control
/embed.jspublic, max-age=3600The one script tag a third-party site installs. An hour stops it being re-fetched per page view and still lets a fix reach existing embeds the same day.curl -sI https://<host>/embed.js | grep -i cache-control
/community/rss.xmlpublic, max-age=0, s-maxage=3600A feed that changes when an entry is written: the browser revalidates every time, the edge holds it for an hour.curl -sI https://<host>/community/rss.xml | grep -i cache-control
/community/feed.xmlpublic, max-age=0, s-maxage=3600The same feed under its second name (the route re-exports the one above), so both addresses answer with the same policy rather than the second one falling through to the HTML rule.curl -sI https://<host>/community/feed.xml | grep -i cache-control
/learn/feed.xmlpublic, max-age=0, s-maxage=3600The guides feed added with the rest of the text surfaces: same shape as the catalog feed, same reasoning — revalidate in the browser, hold an hour at the edge.curl -sI https://<host>/learn/feed.xml | grep -i cache-control
/changelog/feed.xmlpublic, max-age=0, s-maxage=3600The studio log feed. It links each item to its own permalink instead of the homepage anchor, which is the one thing the export copy of this feed cannot do.curl -sI https://<host>/changelog/feed.xml | grep -i cache-control
/api/brand/:path*public, max-age=3600Brand JSON and downloadable marks, rebuilt with the site rather than fingerprinted.curl -sI https://<host>/api/brand/<slug> | grep -i cache-control
/og/:path*public, max-age=3600Share cards are generated per slug at build time, so an hour rather than immutability — a card changes when the page behind it does.curl -sI https://<host>/og/<slug> | grep -i cache-control

What these rules do not cover

  • No CDN is attached in this build, so s-maxage only has meaning behind whichever proxy is in front of it.
  • There is no purge API to call, because there is no cache to purge from inside a static build.

When the catalog comes from a CMS

  • Tag every catalog page with the asset slugs it renders, so one asset edit invalidates its own page and the three index pages that list it — not the whole site.
  • Keep the five-minute s-maxage. When pages can change per edit, the honest default is short and boring rather than long and incorrect.
  • Serve JSON from the same tags as the page that renders it, so an API consumer and a browser never disagree about how fresh a record is.
  • Revalidate on write, not on read: a purge that only happens when someone visits is a cache that stays wrong for whoever arrives second.

The one non-caching header rule

The frame policy says out loud that the route exists to be framed; without it a future default CSP would silently break every embed. Noindex keeps the near-identical demo-only pages out of search results, where they would compete with the asset pages that explain them. A per-site allowlist would need a server reading Origin, which this build does not have.

  • /embed/:path* · Content-Security-Policyframe-ancestors *
  • /embed/:path* · X-Robots-Tagnoindex

Verify it yourself

curl -sI https://<host>/pricing | grep -i cache-control
curl -sI https://<host>/_next/static/chunks/<file>.js | grep -i cache-control

The build harness fetches these headers from the running server every batch and compares each one with the rules documented above, so a config edit that drops a header — or moves the catch-all off the first line — fails the batch rather than shipping quietly.

The repeat visit

#405 — a second visit pays for the document (short shared window) and nothing else: fingerprinted URLs mean the browser reuses everything from cache without a round trip.

Repeat-visit requests and where they come from
RequestCache-ControlSecond visit
/ (document)short shared windowrevalidates in the background
/_next/static/chunks/*.jsimmutable, 1 yearfrom disk cache
/_next/static/chunks/*.cssimmutable, 1 yearfrom disk cache
/_next/static/media/*.woff2immutable, 1 yearfrom disk cache