Content Delivery Network Routing Logic
In a headless stack, CDN routing logic decides latency, cache hit ratio, and content freshness — the bottleneck moves from database queries to edge distribution. Before a request reaches origin, the edge must resolve the path deterministically, negotiate locale, and normalize the cache key. Get those wrong and you ship cache stampedes or leak draft content. This topic, part of the Data Fetching & Caching Strategies section, covers the routing patterns that keep a decoupled CMS fast and consistent at the edge.
Integration Contract
A CDN in front of a headless frontend has three jobs, and each one is configured in a different place. It routes requests to the right origin: the rendering application, the CMS delivery API through a proxy, or an asset CDN. It decides which responses to cache and under which key. And it accepts invalidation from your publishing pipeline. Most CDN incidents on headless sites come from those three being configured by different people who never compared notes, so write the contract down in one place.
The routing rules, the cache-key policy and the purge credentials all live in configuration that should be versioned with the application: Terraform or a CDN-specific config file for Fastly, CloudFront and Cloudflare, or vercel.json and middleware for platform-managed CDNs. The environment side is small but sensitive:
# .env: CDN integration contract
CDN_PROVIDER=cloudflare # or fastly, cloudfront, akamai
CDN_ZONE_ID=0b7c3f9d2e5a4b8c9d1e2f3a4b5c6d7e
CDN_PURGE_TOKEN=scoped_to_cache_purge_only # never an account-wide key
CDN_DEFAULT_SMAXAGE=600
CDN_SWR_SECONDS=86400
PREVIEW_HOSTNAME=preview.example.com # separate host, never cached
A purge token scoped to cache purging only is worth insisting on. The revalidation webhook runs in application code that also handles untrusted input, and a leaked account-wide API key there can rewrite DNS.
Path normalization and cache keys
CDNs map requests to cached assets by URL. Query parameters, trailing slashes, and locale prefixes fragment the cache key, forcing needless origin fetches for what is logically one response. Normalize these in routing middleware before the CDN evaluates cache eligibility.
The edge resolves each request along this path before any origin fetch happens:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const isPreview = req.headers.get('x-preview-mode') === 'true';
// Authenticated preview bypasses the shared cache entirely
if (isPreview) {
return NextResponse.next();
}
// Extract and normalize the locale prefix
const localeMatch = pathname.match(/^\/(en|fr|de|ja)/);
const locale = localeMatch ? localeMatch[1] : 'en';
const normalizedPath = pathname.replace(/^\/(en|fr|de|ja)/, '') || '/';
// Rewrite to a canonical path so the cache key is stable
const url = req.nextUrl.clone();
url.pathname = `/${locale}${normalizedPath}`;
const response = NextResponse.rewrite(url);
// Align browser and edge caching directives
response.headers.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400');
response.headers.set('CDN-Cache-Control', 'max-age=3600, s-maxage=3600');
return response;
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Setting both Cache-Control and CDN-Cache-Control lets the edge serve stale content while revalidating in the background — the same contract the client side uses in SWR Stale-While-Revalidate Patterns.
Framework-agnostic edge routing
Framework middleware fits a monolithic deploy, but distributed setups route at the network edge. Cloudflare Workers and Vercel Edge Functions intercept at the POP and select an origin by CMS environment, content type, or header.
// worker.ts
import { Context, Hono } from 'hono';
const app = new Hono();
app.get('/cms/*', async (c: Context) => {
const url = new URL(c.req.url);
const locale = url.pathname.match(/^\/cms\/(en|de|ja)/)?.[1] || 'en';
const path = url.pathname.replace(/^\/cms\/(en|de|ja)/, '') || '/';
// Pick the CMS origin by locale
const origin = locale === 'de'
? 'https://cdn.contentful.com/spaces/DE_SPACE/environments/master'
: 'https://cdn.contentful.com/spaces/US_SPACE/environments/master';
const cmsUrl = new URL(path, origin);
cmsUrl.searchParams.set('locale', locale);
cmsUrl.searchParams.set('access_token', c.env.CONTENTFUL_TOKEN);
const cmsResponse = await fetch(cmsUrl.toString(), {
headers: { 'Accept': 'application/json' },
cf: { cacheEverything: true, cacheTtlByStatus: { '200-299': 3600 } }
});
const response = new Response(cmsResponse.body, cmsResponse);
response.headers.set('X-Cache-Status', cmsResponse.headers.get('CF-Cache-Status') || 'MISS');
response.headers.set('Vary', 'Accept-Encoding, Accept-Language');
return response;
});
export default app;
Vary keeps language negotiation from colliding on one cache entry; cf.cacheEverything caches the JSON payload. Geo-targeted content routing with edge functions extends this by reading cf-ipcountry to serve region-specific content without duplicating origins.
Multi-region origins and preview isolation
Global deployments resolve origin by data residency, proximity, and CMS availability zone. Routing logic for multi-region CDN content delivery load-balances across regional CMS instances while holding cache consistency.
Isolate preview from live content. Appending ?preview=true fragments the cache key; evaluate x-preview-mode or Authorization instead, and when preview is detected, bypass the cache and route straight to the CMS draft endpoint. Public cache keys stay deterministic and drafts never leak.
Client-side coherence and cache warming
Edge routing fixes the first response; client hydration must match it. React Query for CMS Data deduplicates and background-refetches off cache keys, so the client fetcher must mirror the edge’s path normalization or the two caches diverge.
// useCmsContent.ts
import { useQuery } from '@tanstack/react-query';
interface CmsContent {
id: string;
locale: string;
fields: Record<string, unknown>;
}
export function useCmsContent(path: string, locale: string = 'en') {
// Mirror the edge's normalization so keys match
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
const cacheKey = `cms:${locale}:${normalizedPath}`;
return useQuery<CmsContent>({
queryKey: [cacheKey],
queryFn: async () => {
const res = await fetch(`/api/cms${normalizedPath}?locale=${locale}`);
if (!res.ok) throw new Error('CMS fetch failed');
return res.json();
},
staleTime: 1000 * 60 * 5, // Align with edge max-age
refetchOnWindowFocus: false,
});
}
To beat cold-start latency on new POPs, warm the cache before traffic arrives. Cache warming strategies for global CDN distribution fire parallel HEAD/GET requests from CI/CD and CMS webhooks during deploy, so the first real user hits a warm cache instead of a synchronous origin fetch.
Locale Routing at the Edge
Locale detection is the routing decision with the biggest effect on cache efficiency. There are three signals, in decreasing order of reliability: an explicit locale in the path (/de/pricing), a stored preference cookie set when the reader chose a language, and the Accept-Language header. Only the first belongs in the cache key. The edge should use the other two to redirect an unprefixed request to a prefixed URL once, and then serve prefixed URLs from cache with no further negotiation. Varying cached responses on Accept-Language directly creates one cache entry per distinct header value, and browsers send hundreds of distinct values.
Redirects themselves can be cached per signal combination if they are small, but many teams prefer to compute them at the edge on every request to the root, which is cheap in an edge function and avoids caching mistakes. Geography is a separate concern from language. A reader in Switzerland may want French, German or Italian, and the country is better used for content variants such as pricing and legal notices than for choosing a language. The geo-targeted routing guide covers country-based variants, and dynamic route mapping for headless i18n covers the locale-prefixed URL scheme itself.
Platform-Managed Edges versus Your Own CDN
Vercel, Netlify and Cloudflare Pages run the CDN for you, integrated with the framework: ISR revalidation purges the platform cache automatically, and middleware runs at the edge without separate configuration. That removes most of the invalidation plumbing described above, at the cost of less control over cache keys and purge timing. Running your own CDN in front of a self-hosted or containerized frontend gives full control over keys, tags, shields and failover, and makes you responsible for every purge. Some large sites combine both: a platform for rendering and an enterprise CDN in front for traffic management. In that layout, purges must reach both tiers, and the outer CDN should respect the platform’s cache headers rather than override them with long fixed TTLs.
Caching & Invalidation Strategy
Routing and invalidation are two halves of one design. The cache key decides what a purge has to hit, so the key policy and the purge strategy must be chosen together. Three purge models are available, and CDNs support them unevenly:
Tag purges attach logical tags such as article:7Ht2 and author:42 to responses and purge by tag. They match CMS content naturally, because one entry appears on many URLs. Path purges invalidate URLs or URL patterns. They work when content maps cleanly to routes, and they are the only option on some CDNs. Soft purges mark objects stale instead of deleting them, so the edge can keep serving the old copy under stale-while-revalidate or stale-if-error while it fetches the new one. Where available, soft purges are the safest default for content.
The ordering matters as much as the model. Purge the CDN only after the application cache has regenerated, or the first edge miss after the purge refetches a stale origin page and caches it for a full TTL. The distributed CDN invalidation guide covers the queue and debounce design, and the edge stale-while-revalidate guide covers the header semantics.
Schema & Content Modeling Considerations
Routing depends on the content model more than it appears. Slugs, locales and content types all end up in URLs and cache keys, so their rules must be stable. Model slugs as unique per locale and content type, validate their format in the CMS so they never contain characters that CDNs normalize differently, and keep a history of previous slugs so the edge can redirect old URLs with a 301 instead of serving a cached 404. The redirect handling guide shows how to publish those redirects to the edge.
Content types that vary per visitor, such as personalized banners, A/B variants and geo-specific pricing, should be modeled as separate entries or variant fields, not as logic hidden in the frontend. The edge can then select a variant by a small set of request attributes (country, segment cookie, experiment bucket) and include exactly those attributes in the cache key. Anything that varies by an unbounded attribute, such as a user id, does not belong in a shared cache at all.
Preview & Draft Workflow
Preview traffic must never share a cache key with published traffic. The strongest isolation is a separate hostname such as preview.example.com, configured at the CDN to bypass caching entirely and protected by authentication, so draft URLs cannot leak into search engines or shared caches even by accident. Where a separate host is not possible, route on a header or cookie that the edge checks before the cache lookup, as the middleware above does, and never on a query parameter that can be copied into a public link. The general draft/publish state rules apply, and the authenticated bypass guide covers cookie-based bypass in detail.
Error Handling & Resilience
The CDN is the best place to absorb origin failures, because it already holds copies of most pages. Send stale-if-error alongside stale-while-revalidate so the edge serves the last good copy when the origin returns 5xx or times out. Configure origin timeouts explicitly, short enough that a hung origin does not tie up edge connections, and set up origin failover where the CDN supports it: a secondary origin, such as a static export of the site in object storage, can serve the most important pages during a full outage. Custom error pages at the edge should be cached briefly and never replace a stale-but-valid page.
Testing & Observability
Routing rules are code and deserve tests. Unit-test normalization functions with a table of input and expected cache keys, including trailing slashes, mixed-case locales and tracking parameters. Run a staging check that requests the same logical page through several URL variants and asserts one cache entry, using the CDN’s cache-status and key debug headers. The automated testing for headless integrations topic covers the end-to-end publish check, which is the best proof that purges reach the edge.
In production, watch hit ratio per content type, origin request rate and purge API errors. A falling hit ratio after a deploy usually means a new query parameter or cookie started fragmenting keys. A rising origin rate right after publishes means purges are too broad, and purge API errors mean publishes are silently not reaching the edge.
Production checklist
- Normalize paths before the CDN evaluates them so trailing slashes and query params don’t fragment the cache.
- Separate preview and production traffic by header, never by URL mutation.
- Align
max-ageandstale-while-revalidateacross browser, edge, and origin to avoid conflicting states. - Mirror edge normalization in client fetchers so the two caches stay coherent.
- Wire CMS webhooks to CDN purge APIs for targeted invalidation instead of full flushes.
- Audit
Vary,Cache-Control, andCDN-Cache-Controlagainst the MDN HTTP Caching guide.
Deterministic path resolution, header-based preview isolation, and edge/client cache coherence are what turn a decoupled CMS into a fast, predictable site at scale.
Frequently Asked Questions
Should the CDN cache CMS API responses or only rendered pages?
Both can be cached, with different rules. Rendered HTML is what readers load, so its cache and purges matter most. API responses fetched from the browser, through a proxy, benefit from short edge caching to protect CMS rate limits. Never cache responses fetched with preview tokens.
What is the difference between Cache-Control and CDN-Cache-Control?
Cache-Control applies to every cache, including browsers. CDN-Cache-Control, and provider-specific variants such as Surrogate-Control, apply only to shared caches, which lets you give the edge a long TTL while keeping browsers on a short one that is easy to refresh.
How many cache-key dimensions are too many?
Every dimension multiplies the number of cached variants and divides the hit ratio. Locale and device class are usually fine; country, experiment bucket and segment together can explode the key space. Add a dimension only when the response really differs, and prefer routing variants to separate paths.
Can I use the CMS vendor’s CDN instead of my own?
The CMS vendor’s CDN caches API responses, not your rendered pages, and you cannot control its keys or purges. It protects the CMS, not your site. You still need your own CDN, or a platform-managed one, in front of the frontend.
How do I stop tracking parameters from fragmenting the cache?
Normalize them out of the cache key at the edge: strip utm_*, gclid, fbclid and similar parameters before the lookup, while still forwarding them to analytics in the browser. Keep an allow-list of parameters that really change the response, such as pagination or search terms, and drop everything else from the key.
Where should redirects for renamed slugs live?
At the edge, generated from the CMS. Publish a redirect map whenever a slug changes and load it into an edge key-value store or the CDN’s redirect rules. Serving 301s at the edge keeps old links fast and stops stale 404s from being cached for renamed pages.
How do I debug which cache key a request used?
Most CDNs expose a debug header or trace mode that returns the computed key, such as Fastly’s debug headers or Cloudflare’s trace tool. Enable it on staging, request a page through several URL variants and compare the keys. Differences point directly at the normalization rule that is missing.
How do I roll out a new cache-key rule safely?
Deploy it to a small percentage of traffic or a single region first, and compare hit ratio and origin load against the rest. Key changes empty the cache for affected pages, so schedule them outside traffic peaks and warm the top pages right after.
Should API proxy routes and pages use the same TTLs?
Not necessarily. Pages benefit from long edge TTLs with purges, while API proxy routes used by client-side fetchers often need short TTLs because they are harder to purge by tag. Set them separately and document why.