fix(catalog): let the CDN cache anonymous catalog pages#656
Conversation
Anonymous catalog HTML fell through to the blanket no-store default, so every crawler hit paid for a full SSR render. Emit the existing PUBLIC/PRIVATE_LOCALE_CATALOG_HEADERS based on the request auth signal instead: anonymous GETs become briefly CDN-cacheable while authenticated renders stay private and uncacheable. Closes #654
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
life-ustc | f412c8d | Commit Preview URL Branch Preview URL |
Jul 25 2026, 06:46 PM |
|
E2E HTML report is ready: https://life-ustc.github.io/e2e-snapshot-artifacts/reports/30170241189/index.html |
Browsers may serve an SWR-cached document without revalidating, which breaks flows that mutate state and expect a fresh render on the next navigation (locale switch, login, subscribe) and failed the catalog E2E suites. Keep SWR only on the CDN-facing header.
|
Closing as unsafe to merge and superseded by #657. Production-style probes against this branch returned CDN HIT responses for anonymous HTML even when the follow-up request carried Cookie or Bearer credentials. The cached response also reused locale-dependent HTML, the CSP nonce, and x-request-id, while the cache key did not represent those request differences. That creates auth-boundary, privacy, and response-integrity risks. Safe shared-page caching should stay behind the gateway architecture tracked in #507 and #573; #657 removes the public preview ingress responsible for the dominant Worker CPU load without caching user-facing HTML. |
Summary
Fixes #654.
Anonymous GETs for public catalog pages (
/catalog,/catalog/*) previously fell through to the blanketCache-Control: no-storeinhooks.server.ts, so every request — including the heavy AI-crawler traffic from #652 — ran the full Worker pipeline (hooks, Postgres queries, SSR) with zero edge caching.Change
src/hooks.server.ts+src/lib/public-cache-control.ts:GETrequests to/catalog//catalog/*that don't already setCache-Controlnow get the newPUBLIC_LOCALE_CATALOG_PAGE_HEADERS:Cache-Control: public, max-age=0— browsers must revalidate every navigation (nostale-while-revalidate; see below)Cloudflare-CDN-Cache-Control: public, max-age=60, stale-while-revalidate=300— the edge keeps a short TTL + SWR windowVary: Accept-Language, CookiehasRequestAuthSignalcheck) getPRIVATE_LOCALE_CATALOG_HEADERSinstead:private, no-store+Cloudflare-CDN-Cache-Control: no-store, so personalized renders can never be cached.no-storedefault; routes that set their ownCache-Control(robots.txt, sitemap.xml, API routes) are untouched.__datarequests are unaffected.Why no
stale-while-revalidatefor the browserThe first version of this PR sent
public, max-age=0, stale-while-revalidate=300to browsers and failed the catalog E2E shards deterministically: Chromium may serve an SWR-cached document without revalidating, so flows that mutate state and expect a fresh SSR on the next navigation (locale switch, login, subscribe/unsubscribe, preference autosave) acted on stale pages. SWR now lives only onCloudflare-CDN-Cache-Control, where it is safe.Deploy note (infra step, not in this PR)
Cloudflare does not cache extensionless HTML by default, so the
Cloudflare-CDN-Cache-Controlheader only takes effect once a zone Cache Rule makes these paths eligible for cache, e.g.:http.host eq "life-ustc.tiankaima.dev" and starts_with(http.request.uri.path, "/catalog") and not http.cookie contains "better-auth." and not http.cookie contains "session"The cookie guard in the rule mirrors the origin-side auth check, so logged-in users can never be served cached anonymous HTML. Until the rule exists this PR is a no-op at the CDN (headers only).
Tests
tests/unit/catalog-cache-control.test.ts: anonymous catalog → public CDN headers; authenticated catalog → private/no-store; non-catalog → no-store; explicit routeCache-Controlpreserved.biome check,tsc(typecheck + tests configs), and catalog E2E shards — see CI.Expected impact
Crawler/bot traffic on catalog pages becomes mostly edge cache hits (~0 ms Worker CPU) instead of full SSR renders.