Geo-targeted content routing with edge functions
Geo-targeted routing resolves a visitor’s region at the CDN edge and rewrites the request before it reaches the origin, so a single static build can serve localized content without a per-locale rebuild or a client-side hydration round-trip. Edge middleware reads the geolocation header the CDN already attached, maps it to a content variant, and forwards the request — no waterfall fetch, no origin latency.
How the edge resolves a region
The CDN injects a geolocation header on every request: CF-IPCountry (Cloudflare), x-vercel-ip-country (Vercel), or a Fastly-Client-IP lookup (Fastly). The edge function reads that value, maps it to a CMS content variant, and rewrites the upstream path. Aligning this with Content Delivery Network Routing Logic keeps cache keys split cleanly by region while the CMS still serves one canonical content graph.
Region resolution, fallback chain, and crawler normalization:
flowchart TD
Req["Request at edge"] --> Bot{"Bot user-agent?"}
Bot -->|yes| Default["Normalize to DEFAULT region"]
Bot -->|no| Geo["Read geo header"]
Geo --> Map["Inject geo_region param"]
Default --> Map
Map --> Vary["Rewrite + set Vary: geo header"]
Vary --> Resolve{"Variant exists?"}
Resolve -->|"DE found"| Serve["Serve regional content"]
Resolve -->|"404 / 204"| EU["Fall back to EU"]
EU --> DEF["Fall back to DEFAULT"]
DEF --> Serve
The failure mode is a missing Vary: if the response doesn’t declare that the geo header changed the payload, the CDN treats the path as globally cacheable and serves one region’s content to everyone.
Implementation
This Vercel Edge Middleware reads the country header, injects a geo_region query parameter for the CMS resolver to filter on, and sets Vary so the edge keys the cache per region.
import { NextRequest, NextResponse } from 'next/server';
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
export function middleware(req: NextRequest) {
const region = req.headers.get('x-vercel-ip-country') || 'DEFAULT';
// Inject region so the downstream CMS resolver can filter on it
const url = req.nextUrl.clone();
url.searchParams.set('geo_region', region);
const response = NextResponse.rewrite(url);
// Serve cached for 5 min, then up to 1h stale while refetching
response.headers.set(
'Cache-Control',
'public, s-maxage=300, stale-while-revalidate=3600'
);
// Key the cache by region or every locale collides on the same path
response.headers.set('Vary', 'x-vercel-ip-country');
return response;
}
The middleware runs before the router, so REST or GraphQL resolvers filter by geo_region without a separate build pipeline. s-maxage targets the shared CDN cache; stale-while-revalidate lets the edge serve cached content while it fetches the new variant.
Cache key isolation
Omit the geo header from Vary and the CDN serves localized content to the wrong audience. Per the Vary specification, the header must list every request header that changes the response. Validate Vary propagation in your CDN dashboard and check cache hit ratios per region.
Build a fallback chain in the resolver for missing variants. If geo_region=DE returns 404 or 204, query geo_region=EU, then DEFAULT — otherwise a background revalidation can cache an empty response.
Coalesce revalidation requests
A single cache miss can fan out into concurrent origin fetches from multiple edge nodes, exhausting the CMS rate limit. Cloudflare and Fastly collapse identical in-flight requests into one upstream fetch per cache key; enable it. Parse the CMS’s X-RateLimit-Remaining and Retry-After at the edge and back off s-maxage when quota runs low. For GraphQL backends, persisted queries cut payload size and execution cost during revalidation storms.
Crawler routing
Search engines crawl from centralized IP pools that bypass geo-routing, producing hreflang mismatches and duplicate-content signals. Normalize bot traffic to one region so crawlers index a consistent graph:
const isBot = /bot|crawl|spider|slurp|teoma/i.test(req.headers.get('user-agent') || '');
const resolvedRegion = isBot ? 'DEFAULT' : region;
Pair this with hreflang tags in your layout so regional signals survive without fragmenting crawl budget.
Validation
Automated Testing for Headless Integrations must cover routing and cache behavior. In CI:
- Assert
Varyheaders match the routing logic. - Validate CMS response codes for missing regional variants.
- Simulate concurrent requests and confirm the CDN coalesces them.
- Confirm
hreflangand canonical tags match the resolved region.
Playwright or Cypress can intercept edge requests to assert s-maxage and stale-while-revalidate compliance.
Run routing at the edge, declare every geo dimension in Vary, give the resolver a fallback chain, and respect the CMS rate limit, and one static build serves every market with a cache hit on the first request.