Context
Public pages, such as a marketing site or a tenant storefront, are React Router 7 framework-mode apps that a Cloudflare Worker serves (0165). The console is a static single-page app and stays one (0052). 0176 planned later prerendering from the experience service. Three questions needed a rule: when a page may be prerendered, what is cached, and how a locale or market is chosen. A prerendered page is one file that every visitor to its URL receives, so it is correct only for a site fixed at build time. Caching the HTML of a multi-tenant page shares one tenant’s content with another tenant’s visitors unless the key covers everything that changes the page. A locale or market chosen by cookie, language header or IP address gives one URL different answers, which breaks shared caches and crawlers.Decision
- Web renders run on Cloudflare Workers. A React Router 7 framework-mode app is served by a Worker. The console is excluded: it stays a static single-page app.
- Prerendering needs a fixed site. A route may set
prerender: trueonly whenSiteConfig.fixedSiteIdis set. Gen writesrun_worker_first: ['/*', '!/assets/*']for a web member, so the Worker sees every page request (for CSP, redirects and headers) and only the asset paths skip it. - Cache data, not HTML. A query takes a cache profile:
ctx.query(Query, input, { cache: CacheProfile }). The key covers the query, its input, the site, the locale, the market and the version, so two visitors who would see different content never share an entry. Entries are served stale while they refresh, and concurrent misses for one key share one origin call (single-flight). A Workers cache driver in@stackra/cacheimplements this; it is one more driver beside the four in 0150. HTML responses areCache-Control: private, no-cache. - Locale and market paths are static, with no HTML redirects.
- Each locale has its own path prefix, a lowercase BCP 47 tag such as
/ar-sa/. A sub-folder market puts the locale and the market in that first segment. - The server never redirects an HTML request because of a cookie, the
Accept-Languageheader or the visitor’s IP address. A page offers the other locale in a suggestion banner, which the browser renders. - Responses send no
Varyheader, because the URL alone selects the content. - Server path parsers strip the
.datasuffix that the router adds to data requests before they match a locale or a route.
- Each locale has its own path prefix, a lowercase BCP 47 tag such as
- React Router middleware is on, with typed context. The middleware future flag is enabled in the React Router config. Values that middleware passes down are typed context keys, not an untyped bag.
- The Worker forwards three things to the API.
- The W3C
traceparentheader, so one edge request and the service calls it makes share one trace (0184). - The visitor’s IP address, signed with an HMAC by the Worker. The API verifies the signature before it uses the address, so a client cannot set the address by sending its own header.
- The
Sec-Purposeheader, so the API can tell prefetch and prerender requests from real visits.
- The W3C
Consequences
- A prerendered page is the same for every visitor. A page that needs visitor-specific or tenant-specific data fetches it through a cached query.
- The locale is part of the URL, so a link can be shared and cached as it is. A visitor who lands on the wrong locale sees a banner and keeps the page they asked for.
- No shared cache stores a page: HTML is
private, no-cache(decision 3), so a browser revalidates it and a shared cache passes it through. What a shared cache keeps is the static assets, one copy per URL because no response varies, and the query data of decision 3 under its full key. - The Worker runs for every page request, so a failure in it affects every page.
Enforcement
planned — gen refusesprerender: true without SiteConfig.fixedSiteId and writes the run_worker_first value above; a test per
cache-key part (query, input, site, locale, market, version); a test that no HTML GET response sets a cookie, asserted in the
end-to-end suite; and a test that the API rejects a visitor address whose signature does not verify. Linear: STA-341.
Alternatives
- Prerender once for each known site at build time: not chosen. Decision 2 is the one condition for prerendering.
run_worker_first: truefor every path: not chosen, because asset requests would run the Worker too.- Cache full pages at the Worker: not chosen. A key mistake would show one tenant’s page to another tenant’s visitors.
- Browser caching only: not chosen. The browser cache is per visitor and does not share a fetched result across visitors.
- Markets by domain only: not chosen. The first path segment carries the market for sub-folder markets (decision 4).
- A geo-IP redirect on
/only: not chosen. Decision 4 forbids redirects on visitor signals. - A service binding that passes the address in the call context: not chosen; decision 6 forwards headers instead.
- Trusting
CF-Connecting-IPon the API zone: not chosen. A signature lets the API check where the address came from; trusting one header does not.