Geolocation Routing
Geo-based routing and personalization for delivering localized content at the edge
You are an expert in geo-based routing and personalization for building edge-first applications that deliver localized, region-aware content. ## Key Points - **Country** (ISO 3166-1 alpha-2) - **Region/state** (ISO 3166-2) - **Latitude/longitude** - **Timezone** - **ASN** (Autonomous System Number) - **Postal code** (on some platforms) - **Always provide a fallback locale/region** — geo data may be missing for VPN users, bots, or edge cases. Default to a sensible region. - **Let users override their detected location** — store locale preference in a cookie so users can switch away from the auto-detected region. - **Cache geo-specific responses with `Vary`** — if the response changes by country, set `Vary: X-Country` (using a custom header) or use per-country cache keys. - **Use ISO country codes consistently** — avoid mapping to full country names in routing logic; stick with the two-letter ISO 3166-1 codes. - **Keep geo-routing deterministic** — given the same inputs (country, path, cookies), always return the same result. Avoid randomness in redirects. - **Validate compliance requirements with legal counsel** — GDPR, data residency, and sanctions lists change. Do not hardcode legal decisions; load them from configuration.
skilldb get edge-computing-skills/Geolocation RoutingFull skill: 398 linesGeolocation Routing — Edge Computing
You are an expert in geo-based routing and personalization for building edge-first applications that deliver localized, region-aware content.
Overview
Geolocation routing uses the physical location of a request — derived from IP address, CDN-injected headers, or the GPS coordinates of the user — to make routing decisions, personalize content, and enforce regional policies at the edge. Edge platforms inject geolocation data into requests before they reach your code, enabling sub-millisecond routing decisions without additional API calls.
Geolocation data available at the edge typically includes:
- Country (ISO 3166-1 alpha-2)
- Region/state (ISO 3166-2)
- City
- Latitude/longitude
- Timezone
- ASN (Autonomous System Number)
- Postal code (on some platforms)
Core Concepts
Accessing Geo Data by Platform
Cloudflare Workers:
export default {
async fetch(request: Request): Promise<Response> {
const cf = (request as any).cf;
const geo = {
country: cf?.country, // "US"
region: cf?.region, // "California"
regionCode: cf?.regionCode, // "CA"
city: cf?.city, // "San Francisco"
postalCode: cf?.postalCode, // "94107"
latitude: cf?.latitude, // "37.7749"
longitude: cf?.longitude, // "-122.4194"
timezone: cf?.timezone, // "America/Los_Angeles"
asn: cf?.asn, // 13335
continent: cf?.continent, // "NA"
};
return Response.json(geo);
},
};
Vercel Edge (Next.js Middleware):
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const geo = {
country: request.geo?.country, // "US"
region: request.geo?.region, // "CA"
city: request.geo?.city, // "San Francisco"
latitude: request.geo?.latitude, // "37.7749"
longitude: request.geo?.longitude, // "-122.4194"
};
const response = NextResponse.next();
response.headers.set("X-Geo-Country", geo.country ?? "unknown");
return response;
}
Deno Deploy:
Deno.serve((request: Request) => {
// Deno Deploy does not inject geo headers by default.
// Use a geo-IP lookup or place the app behind Cloudflare for geo data.
// Alternatively, use the cf-ipcountry header if proxied through Cloudflare.
const country = request.headers.get("cf-ipcountry") ?? "unknown";
return new Response(`Country: ${country}`);
});
Implementation Patterns
Country-Based Redirects
const COUNTRY_DOMAINS: Record<string, string> = {
DE: "https://de.example.com",
FR: "https://fr.example.com",
JP: "https://jp.example.com",
GB: "https://uk.example.com",
};
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const cf = (request as any).cf;
const country = cf?.country as string | undefined;
// Only redirect from the main domain
if (url.hostname !== "www.example.com") {
return fetch(request);
}
// Don't redirect API calls
if (url.pathname.startsWith("/api")) {
return fetch(request);
}
// Check for override cookie (user explicitly chose a locale)
const cookies = request.headers.get("cookie") ?? "";
if (cookies.includes("locale-override=")) {
return fetch(request);
}
const targetDomain = country ? COUNTRY_DOMAINS[country] : undefined;
if (targetDomain) {
const redirectUrl = `${targetDomain}${url.pathname}${url.search}`;
return Response.redirect(redirectUrl, 302);
}
return fetch(request);
},
};
Regional Content Personalization
interface RegionConfig {
currency: string;
locale: string;
dateFormat: string;
supportPhone: string;
legalEntity: string;
}
const REGION_CONFIGS: Record<string, RegionConfig> = {
US: {
currency: "USD",
locale: "en-US",
dateFormat: "MM/DD/YYYY",
supportPhone: "+1-800-555-0100",
legalEntity: "Example Inc.",
},
GB: {
currency: "GBP",
locale: "en-GB",
dateFormat: "DD/MM/YYYY",
supportPhone: "+44-800-555-0100",
legalEntity: "Example Ltd.",
},
DE: {
currency: "EUR",
locale: "de-DE",
dateFormat: "DD.MM.YYYY",
supportPhone: "+49-800-555-0100",
legalEntity: "Example GmbH",
},
JP: {
currency: "JPY",
locale: "ja-JP",
dateFormat: "YYYY/MM/DD",
supportPhone: "+81-800-555-0100",
legalEntity: "Example K.K.",
},
};
const DEFAULT_REGION: RegionConfig = REGION_CONFIGS["US"];
export default {
async fetch(request: Request): Promise<Response> {
const cf = (request as any).cf;
const country = cf?.country as string ?? "US";
const config = REGION_CONFIGS[country] ?? DEFAULT_REGION;
const response = await fetch(request);
return new HTMLRewriter()
.on("[data-region-currency]", {
element(el) {
el.setInnerContent(config.currency);
},
})
.on("[data-region-phone]", {
element(el) {
el.setInnerContent(config.supportPhone);
},
})
.on("[data-region-legal]", {
element(el) {
el.setInnerContent(config.legalEntity);
},
})
.on("html", {
element(el) {
el.setAttribute("lang", config.locale);
},
})
.transform(response);
},
};
Geo-Fencing and Compliance
const GDPR_COUNTRIES = new Set([
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
"PL", "PT", "RO", "SK", "SI", "ES", "SE",
]);
const BLOCKED_COUNTRIES = new Set(["KP", "IR", "SY", "CU"]);
export default {
async fetch(request: Request): Promise<Response> {
const cf = (request as any).cf;
const country = cf?.country as string | undefined;
// Block sanctioned countries
if (country && BLOCKED_COUNTRIES.has(country)) {
return new Response("Service not available in your region", { status: 451 });
}
const response = await fetch(request);
// Inject GDPR consent banner for EU visitors
if (country && GDPR_COUNTRIES.has(country)) {
const cookies = request.headers.get("cookie") ?? "";
if (!cookies.includes("consent=accepted")) {
return new HTMLRewriter()
.on("body", {
element(el) {
el.append(
`<div id="consent-banner" style="position:fixed;bottom:0;width:100%;background:#333;color:#fff;padding:16px;z-index:9999;">
We use cookies to improve your experience.
<button onclick="document.cookie='consent=accepted;max-age=31536000;path=/';this.parentElement.remove();">Accept</button>
</div>`,
{ html: true }
);
},
})
.transform(response);
}
}
return response;
},
};
Nearest Origin Routing
Route to the closest backend server based on user location:
interface OriginRegion {
url: string;
lat: number;
lon: number;
}
const ORIGINS: OriginRegion[] = [
{ url: "https://us-east.api.example.com", lat: 39.0438, lon: -77.4874 },
{ url: "https://eu-west.api.example.com", lat: 53.3331, lon: -6.2489 },
{ url: "https://ap-southeast.api.example.com", lat: 1.3521, lon: 103.8198 },
];
function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function findNearestOrigin(lat: number, lon: number): string {
let nearest = ORIGINS[0];
let minDistance = Infinity;
for (const origin of ORIGINS) {
const distance = haversineDistance(lat, lon, origin.lat, origin.lon);
if (distance < minDistance) {
minDistance = distance;
nearest = origin;
}
}
return nearest.url;
}
export default {
async fetch(request: Request): Promise<Response> {
const cf = (request as any).cf;
const lat = parseFloat(cf?.latitude ?? "0");
const lon = parseFloat(cf?.longitude ?? "0");
const originUrl = findNearestOrigin(lat, lon);
const url = new URL(request.url);
return fetch(`${originUrl}${url.pathname}${url.search}`, {
method: request.method,
headers: request.headers,
body: request.body,
});
},
};
Localized Pricing with Exchange Rates
interface Env {
PRICING_KV: KVNamespace;
}
interface ExchangeRates {
[currency: string]: number;
}
const COUNTRY_CURRENCY: Record<string, string> = {
US: "USD", GB: "GBP", DE: "EUR", FR: "EUR", JP: "JPY",
AU: "AUD", CA: "CAD", BR: "BRL", IN: "INR", KR: "KRW",
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const cf = (request as any).cf;
const country = cf?.country as string ?? "US";
const currency = COUNTRY_CURRENCY[country] ?? "USD";
const basePriceUsd = 49.99;
if (currency === "USD") {
return Response.json({ price: basePriceUsd, currency: "USD" });
}
const rates = await env.PRICING_KV.get<ExchangeRates>("exchange-rates", "json");
const rate = rates?.[currency] ?? 1;
const localPrice = Math.round(basePriceUsd * rate * 100) / 100;
return Response.json({
price: localPrice,
currency,
country,
});
},
};
Best Practices
- Always provide a fallback locale/region — geo data may be missing for VPN users, bots, or edge cases. Default to a sensible region.
- Let users override their detected location — store locale preference in a cookie so users can switch away from the auto-detected region.
- Cache geo-specific responses with
Vary— if the response changes by country, setVary: X-Country(using a custom header) or use per-country cache keys. - Use ISO country codes consistently — avoid mapping to full country names in routing logic; stick with the two-letter ISO 3166-1 codes.
- Keep geo-routing deterministic — given the same inputs (country, path, cookies), always return the same result. Avoid randomness in redirects.
- Validate compliance requirements with legal counsel — GDPR, data residency, and sanctions lists change. Do not hardcode legal decisions; load them from configuration.
Common Pitfalls
- Redirect loops — user is redirected to
de.example.combased on IP, butde.example.comalso runs the same geo check and redirects back. Always skip redirects when the user is already on the correct domain. - Breaking SEO with geo-redirects — search engine crawlers may originate from a single country. Use
hreflangtags and return 200 responses withVaryheaders instead of hard redirects for content pages. - Assuming IP geolocation is precise — IP-based location is accurate to the country level but unreliable at the city level. Do not use it for street-level decisions.
- Caching geo-personalized content without per-country cache keys — a response personalized for Germany gets cached and served to a user in Japan. Always include the country in the cache key.
- VPN and proxy users getting wrong content — users behind VPNs appear in a different country. Provide a manual locale selector and respect it via cookies.
- Not accounting for bots and monitoring — health checks and uptime monitors hit from fixed locations. Exclude known bot user agents or monitoring IPs from geo-routing logic.
Core Philosophy
Geolocation routing is about delivering the right experience to the right user without asking them. When a visitor from Germany sees prices in euros, content in German, and a GDPR consent banner, the application feels locally relevant without requiring manual locale selection. But this automatic personalization must always be overridable — VPN users, expatriates, and multilingual users should be able to choose their preferred locale via a cookie or explicit selector.
Geo data is approximate, not authoritative. IP-based geolocation is reliable at the country level but unreliable at the city level and useless at the street level. Do not make critical decisions (like blocking access or showing different prices) based on city-level precision. Stick to country codes for routing decisions, and always provide fallback behavior for cases where geo data is missing or incorrect.
Compliance decisions must be configurable, not hardcoded. GDPR countries, sanctioned regions, and data residency requirements change as laws evolve. Load these lists from configuration (KV, environment variables, or an API) rather than hardcoding country arrays in your source code. Legal teams should be able to update compliance rules without a code deployment.
Anti-Patterns
-
Creating redirect loops — redirecting based on IP country to a domain that also runs the same geo check causes infinite redirects; always skip geo-routing when the user is already on the correct domain.
-
Breaking SEO with hard geo-redirects — search engine crawlers originate from a single country; hard-redirecting them prevents indexing of alternate locales; use
hreflangtags andVaryheaders instead. -
Caching geo-personalized content without per-country cache keys — a response personalized for Germany gets cached and served to Japanese users; always include the country in the cache key for geo-varied content.
-
Assuming IP geolocation is precise — making street-level decisions based on IP data leads to incorrect behavior for VPN users, mobile users on carrier networks, and anyone behind a corporate proxy.
-
Not providing a manual locale override — forcing users into a geo-detected locale with no way to switch frustrates expatriates, travelers, and multilingual users; always offer a locale selector and persist the choice in a cookie.
Install this skill directly: skilldb add edge-computing-skills
Related Skills
Cloudflare D1
Cloudflare D1 for running SQLite databases at the edge with SQL query support
Cloudflare Kv
Cloudflare Workers KV for globally distributed key-value storage at the edge
Cloudflare Workers
Cloudflare Workers for serverless edge compute using the V8 isolate model
Deno Deploy
Deno Deploy for globally distributed edge applications using the Deno runtime
Edge Auth
Authentication and authorization at the edge for securing requests before they reach the origin
Edge Caching
Edge caching strategies for optimizing content delivery and reducing origin load