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 page covers the routing patterns that keep a decoupled CMS fast and consistent at the edge.
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:
flowchart TD
A["Incoming request"] --> B{"Preview header set?"}
B -->|yes| C["Bypass shared cache, route to draft origin"]
B -->|no| D["Normalize locale prefix and path"]
D --> E["Rewrite to canonical cache key"]
E --> F["Set Cache-Control + CDN-Cache-Control"]
F --> G{"Edge cache HIT?"}
G -->|hit| H["Serve from POP"]
G -->|miss| I["Controlled origin fetch by locale"]
I --> J["Cache payload, set Vary"]
J --> H
// 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.
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.