Skip to content
FREE RESOLE AT 18 MONTHSSHIPS FROM PORTLAND, OR — 2 DAYLOT 26.07 OPEN
LASTFORM
SEARCH

DOCUMENT 07 — RENDERING STRATEGY

How this
site is built

Each route is cached at the shortest interval its data tolerates and no longer. Stock is the only value read per request. The table below is generated from the same constant the render badge in every footer reads, so it cannot describe a plan the site is not running.

Content comes from Sanity, read over GROQ and validated at the boundary. A typed fixture adapter stands behind it: a deploy missing its project credentials degrades to built-in content instead of failing, and a parity test asserts the two adapters answer every method identically, so the fallback cannot quietly drift from the real thing. Which one this build read is in the SOURCE row.

BUILD

COMMIT
e695001
BUILT
2026-08-03 07:21:43 UTC
SOURCE
SANITY
ROUTES
5 CACHED / 5 PER-REQUEST
ISLANDS
7 CLIENT COMPONENTS

Route table

10 ENTRIES — GENERATED FROM lib/rendering.ts
ROUTESTRATEGYREVALIDATEREASONING
/ISR3600 SEditorial content changes hourly at most. The cheapest possible render that is still fresh.
/collections/[slug]SSG3600 SFour finite, high-traffic, rarely changing collections. Build-time cost is bounded; unknown slugs fall back to ISR.
/products/[slug]ISR + ON DEMAND300 SThe spec sheet never changes and price rarely does, so it is cached. Stock is read separately and never cached.
/api/stockEDGENEVERSource of truth for the size grid. A cached stock response is a correctness bug, so this is never cached.
/searchSSRNEVEROutput is a pure function of user input. Caching it is pointless and would serve one visitor another visitor results.
/journal/[slug]SSG + ON DEMANDAT BUILDLong-form editorial, static once published. The publish webhook revalidates it so writers are not blocked on a deploy.
/engineeringSSGAT BUILDGenerated from this manifest at build time, so it cannot describe a rendering plan the site is not running.
/cartCLIENTNEVERPer-user state held in localStorage. No server involvement and no SEO value, so nothing to render or cache.
/studioCLIENTNEVERA static signpost rather than an embedded Studio. Sanity 5.31.1 imports useEffectEvent as a named export that Next 15 will not resolve in a client bundle, so the editor runs standalone via `pnpm sanity dev` against the same schemas.
middleware.tsEDGENEVERRuns before the cache on every request, which is exactly why it stays this cheap: one header read, at most two cookie writes, and no I/O at all.

Core web vitals

THIS SESSION — MEASURED IN YOUR BROWSER

LCP — LARGEST PAINT

— S

NOT YET MEASURED — THRESHOLD 2.5 S

INP — INTERACTION

— MS

NOT YET MEASURED — THRESHOLD 200 MS

CLS — LAYOUT SHIFT

—

NOT YET MEASURED — THRESHOLD 0.1

TTFB — TIME TO FIRST BYTE

— MS

NOT YET MEASURED — THRESHOLD 800 MS

MEASURED LIVE WITH web-vitals ON THIS PAGE LOAD. NOT FIELD DATA — THIS PROJECT HAS NO RUM PIPELINE, AND A 75TH-PERCENTILE FIGURE OVER 28 DAYS WOULD BE INVENTED. BARS SHOW THE VALUE AGAINST GOOGLE’S GOOD THRESHOLD; SHORTER IS BETTER. INP APPEARS ONLY AFTER YOU INTERACT.

JavaScript budget

MEASURED FROM pnpm build
ROUTEFIRST LOADOVER BASELINE
framework baseline102 kB—
/115 kB13 kB
/collections/[slug]134 kB32 kB
/products/[slug]135 kB33 kB
/engineering114 kB12 kB

The original brief set a 90 kB budget for the product page. That is below the floor: Next 15 and React 19 ship 102 kB of shared framework code before a line of application code exists, which is why both numbers appear here. The figure worth defending is the one on the right — what this application adds — and the product page adds 33 kB, comfortably inside 90. Quoting only the total would have made an unreachable target look met or missed for the wrong reason.

Five decisions worth defending

CURRENCY LIVES IN A COOKIE

The obvious design has middleware set a request header that Server Components read via headers(). That call opts a page into dynamic rendering, which would have turned every static and ISR route here into SSR — destroying the thing this site exists to demonstrate. The middleware sets a cookie instead and a client island reads it.

STOCK IS THE ONLY UNCACHED READ

A product page is mostly a spec sheet, and a spec sheet does not change. Stock does, unpredictably. Caching them together means choosing between a stale size grid and an uncacheable page, so they are read separately: the page is ISR, and the size grid refreshes against an edge route that is never cached.

FILTERS LIVE IN THE URL

Facet state is in the query string rather than component state, so a filtered view is shareable, reloadable, and readable on the server. The cost is that reading searchParams renders that request dynamically — the unfiltered collection is prerendered, a filtered one is computed on demand.

GROQ EVERYWHERE, GRAPHQL ONCE

Search is the one route that speaks GraphQL, against a deployed endpoint rather than a stand-in. GROQ projections shape the response so the adapter receives exactly the keys its schema wants; GraphQL returns the document’s own field names and the mapping happens in TypeScript. Both paths exist here on purpose, so the difference is visible rather than asserted.

The difference has teeth. GROQ reads whatever a document holds, so an image field declared inline with its own alt text served every GROQ query correctly and passed schema deployment without a warning — but the extractor does not carry inline fields onto the shared Image type, and alt was simply absent from the deployed API. Naming the type fixed it. A query model that requires every shape to be named finds the shapes you never named.

What made that expensive was not the missing field but how it failed. The adapter turned the schema’s error into an empty result, and an empty result renders as “no model by that name” — a statement about the catalogue, made on the evidence of a broken query. It survived a deploy and a manual check because it looked exactly like a search that had worked. Transport failures now throw and render as unavailable; only a query that genuinely matched nothing says so.

PUBLISHING PURGES TAGS, NOT PATHS

Every cached read declares what it depended on: a product list tags product, a single product page also tags product:its-slug. The webhook purges tags rather than a list of URLs, so it never has to know which pages render which documents — a new route that reads a product cannot forget to invalidate itself, because the dependency is recorded where the read happens rather than in a switch statement somewhere else.

Publishing one product therefore drops that product’s page and every list it appears in. It also drops the other product pages — not over-eagerness: each one renders a related-products strip built from the whole catalogue, so a price change really does make them stale. The precise claim is not “only what changed” but everything whose output depends on what changed, which is the stronger property and the one a cache can actually guarantee.