Caching Model
This page documents how caching is implemented for the smallprotocol.dev documentation. The pattern is reusable for other React Router 7 (RR7) apps deployed to Vercel.
Current Caching Policy
Docs are always fresh during launch stabilization. Both HTML responses and .data loader responses are served with no-store, so the CDN does not cache them.
Where Caching Happens
flowchart LR Browser --> CDN["Vercel CDN (Edge)"] CDN --> Origin["Serverless Function"] CDN -->|"HIT"| Browser CDN -->|"MISS"| Origin Origin --> CDN
With no-store set, Vercel's CDN is bypassed and always forwards requests to the origin.
Cache Control Headers
The docs routes use these headers:
Cache-Control: no-store
Cdn-Cache-Control: no-store
What This Means
- The browser does not cache responses.
- The Vercel CDN does not cache responses.
- Every request goes to the origin, so updates appear immediately after deploy.
Expected Headers
When testing with curl -I, you'll see these headers:
curl -I https://smallprotocol.dev/docs
HTTP/2 200
cache-control: no-store
cdn-cache-control: no-store
x-vercel-cache: MISS
age: 0
Key Headers to Check
| Header | Description |
|---|---|
x-vercel-cache | MISS (origin hit) |
age | Always 0 when uncached |
cache-control | Should be no-store |
cdn-cache-control | Should be no-store |
vary | Headers that affect cache key (usually Accept-Encoding) |
How to Test
Test HTML Document Headers
curl -I https://smallprotocol.dev/docs/small-v1
Test .data Loader Headers
curl -I https://smallprotocol.dev/docs/small-v1.data
Expected Pattern
Every request should return cache-control: no-store, cdn-cache-control: no-store, x-vercel-cache: MISS, and age: 0.
Local Self-Check Script
bun run docs:selfcheck
Override the target URL if needed:
DOCS_BASE_URL=https://smallprotocol.dev bun run docs:selfcheck
Implementation
Loader Headers (Route Level)
Each docs route exports headers that enforce no-store:
// app/modules/docs/routes/doc.route.tsx
import { data as routerData } from "react-router";
import type { HeadersFunction, LoaderFunctionArgs } from "react-router";
const DOCS_NO_STORE = "no-store";
export async function loader({ params }: LoaderFunctionArgs) {
const doc = await loadDocHtml(params.slug ?? "docs");
return routerData(doc, {
headers: {
"Cache-Control": DOCS_NO_STORE,
"Cdn-Cache-Control": DOCS_NO_STORE,
},
});
}
export const headers: HeadersFunction = ({ loaderHeaders }) => {
return {
"Cache-Control": loaderHeaders.get("Cache-Control") ?? DOCS_NO_STORE,
"Cdn-Cache-Control":
loaderHeaders.get("Cdn-Cache-Control") ?? DOCS_NO_STORE,
};
};
Server Response Headers (Entry Server)
The entry.server.tsx ensures docs routes get the correct headers for both HTML and .data responses:
// app/entry.server.tsx
const DOCS_NO_STORE = "no-store";
// In handleRequest:
const { pathname } = new URL(request.url);
const isDocsHtml = pathname === "/docs" || pathname.startsWith("/docs/");
const isDocsData = pathname.endsWith(".data") && pathname.startsWith("/docs");
if (isDocsHtml || isDocsData) {
responseHeaders.set("Cache-Control", DOCS_NO_STORE);
responseHeaders.set("Cdn-Cache-Control", DOCS_NO_STORE);
}
// Ensure CDN variance is explicit
responseHeaders.set("Vary", "Accept-Encoding");
Applying to Other RR7 Apps
To keep content always fresh during stabilization:
1. Define No-Store Policy
const NO_STORE = "no-store";
2. Add to Route Loaders
export async function loader() {
const data = await fetchData();
return routerData(data, {
headers: {
"Cache-Control": NO_STORE,
"Cdn-Cache-Control": NO_STORE,
},
});
}
export const headers: HeadersFunction = ({ loaderHeaders }) => ({
"Cache-Control": loaderHeaders.get("Cache-Control") ?? NO_STORE,
"Cdn-Cache-Control": loaderHeaders.get("Cdn-Cache-Control") ?? NO_STORE,
});
3. Handle in Entry Server
// entry.server.tsx
const { pathname } = new URL(request.url);
const isDocsRoute = pathname.startsWith("/docs");
if (isDocsRoute) {
responseHeaders.set("Cache-Control", NO_STORE);
responseHeaders.set("Cdn-Cache-Control", NO_STORE);
responseHeaders.set("Vary", "Accept-Encoding");
}
4. Verify with curl
curl -I https://your-app.vercel.app/docs
curl -I https://your-app.vercel.app/docs.data
Caveats
- Personalized content: Keep
no-storefor user-specific content - Cache invalidation: Not needed when
no-storeis set - Development: Local dev mirrors no-store behavior; test on preview/production deployments
- Vary header: Always include
Vary: Accept-Encodingfor compressed responses