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:

Region resolution with crawler normalization and fallbacksBots are normalized to the default region, humans are resolved from the CDN's country header; the region is injected into the request and the resolver falls back from country to continent to default when a variant is missing.Request at edgeBot useragent?Region = DEFAULTRead countryheaderInject geo_regionVary on headerResolve variantDE → EU → DEFAULTyesno
Crawlers always see the default region, and a missing regional variant degrades to the next broader one instead of an empty page.

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.

TypeScript
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.

Geo headers by platformThe request header each edge platform provides for the visitor's country, the granularity available, and whether it can be spoofed by the client.PlatformCountry headerFiner granularityClient can spoof?CloudflareCF-IPCountrycity, region in request.cfno, set by edgeVercelx-vercel-ip-countrycity, region headersno, set by edgeFastlyclient.geo.country_code (VCL)city, metro codeno, VCL variableCloudFrontCloudFront-Viewer-Countrycity with policyonly if forwarded unchecked
Only trust headers set by your CDN; strip any client-supplied copy before your function reads it.

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:

Cache entries created per region strategyHow many cache entries one page produces under three keying strategies: per country, per region group and per country with a default collapse for countries without variants.Key by raw countryone per country seen~200 entries per pageKey by region group4 entries per pageEUNAAPACDEFAULTKey by resolved variantonly variants that existDEEUDEFAULT
Keying by the resolved variant, not the raw country, keeps the entry count equal to the number of real variants.
TypeScript
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:

  1. Assert Vary headers match the routing logic.
  2. Validate CMS response codes for missing regional variants.
  3. Simulate concurrent requests and confirm the CDN coalesces them.
  4. Confirm hreflang and 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 hreflang tied 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 hreflang tied 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.