Geo-Targeted Content Routing with Edge Functions
This guide, part of Content Delivery Network Routing Logic, shows how 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:
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.
Configuration Reference
| Setting | Value | Why |
|---|---|---|
| Country header | platform-provided only | Client-supplied headers can be forged; strip them at the edge. |
| Cache key dimension | resolved variant, not raw country | Keeps entries equal to the number of real variants. |
| Fallback chain | country → region group → default | A missing translation or offer never produces an empty page. |
s-maxage |
300 s | Regional offers change more often than articles. |
stale-while-revalidate |
3600 s | Readers never wait on a regional refetch. |
| Crawler handling | default region, consistent hreflang |
Search engines index one stable version per locale. |
The most important change from the implementation above is keying on the resolved variant. Vary: x-vercel-ip-country creates one entry per country that visits, even when only three countries have their own content. Resolve the variant in the middleware first, then rewrite to a variant path such as /_geo/eu/pricing or set a variant header that the cache key uses, so every country without its own variant shares the default entry.
Gotchas & Edge Cases
- Geography is not language. A reader in Belgium may want French, Dutch or German. Use geography for commercial variants such as pricing, availability and legal notices, and use the locale in the URL for language.
- VPNs and travellers. Geo headers reflect the network, not the reader. Offer a visible region switcher that sets a cookie, and let the cookie take priority over the header.
- Legal content. Consent banners and legal notices that must differ by jurisdiction should fail safe: when the region is unknown, show the strictest variant.
- Mixed caching of HTML and data. If the HTML is keyed by region but client-side data fetches are not, hydration can mix regions. Include the resolved region in client query keys too.
- Search engine signals. Serving different content to crawlers than to users in the same locale can look like cloaking. Keep regional differences to offers and notices, not the main content, and keep
hreflangtied to language.
Worked Example
An electronics retailer ran one Next.js build for 28 countries but had regional pricing and shipping promises for only four regions. Their first edge setup varied on the raw country header, which created up to 28 cache entries per product page, cut the hit ratio to under 40 percent and made purges slow. After switching to a resolved variant (DE, UK, US, DEFAULT) injected by middleware and used as the cache key, each page had at most four entries, the hit ratio rose above 90 percent, and a single tag purge per product updated all regions at once.
Rollout Checklist
- Decide which differences are regional (prices, offers, legal) and which are linguistic (the locale in the URL).
- Model regional variants in the CMS as fields or entries tagged with a region code, plus a default.
- Resolve the variant in middleware with a country → region → default chain.
- Key the cache by the resolved variant and include it in client-side query keys.
- Normalize crawlers to the default variant and keep
hreflangtied to language. - Add a region switcher that sets a cookie taking priority over the geo header.
- Monitor hit ratio and entries per page after launch; both reveal a keying mistake quickly.
Whichever layer you change first, measure before and after with the same instruments: the CDN’s cache-status and Age headers for correctness, real-user TTFB per region for impact, and origin request rate for cost. A routing or caching change that improves one of those at the expense of another is usually a keying mistake, and the three together make it visible within a day of rollout.
Frequently Asked Questions
Should the region be in the URL instead of a header?
For language, yes: locale prefixes are crawlable and shareable. For commercial variants of the same language page, a header-driven variant keeps one canonical URL for search while showing correct prices, which is usually what the business wants.
How do I test geo routing without travelling?
Most platforms let you override geolocation in development or staging, for example by sending the country header to a preview deployment that trusts it. In production, use synthetic monitors in several regions and assert the variant marker in the response.
What happens when the CDN cannot determine the country?
The header is missing or set to an unknown code such as XX or T1 for Tor. Treat it like any other unresolved region and fall back to the default variant, or the strictest one for legal content.
Can geo routing and experiments share one middleware?
Yes, and they should, because both change the cache key. Resolve region first, then experiment bucket, and build a single variant path or key from both. Keep the combined number of variants per page small; region times experiment multiplies quickly, and every combination is another cache entry and another object to purge on publish.