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.