Geo-Based Redirects in Next.js 16 proxy.ts
Use proxy.ts and request geo headers to redirect users to a country-specific path or domain. Works with Vercel and any geo-aware host.
A proxy.ts that reads the user's country from request headers and redirects to a localized path. This is the canonical pattern for routing visitors to /in/, /uk/, or /us/ based on geolocation. It runs before any page renders, so the redirect is fast and the SEO impact is clean.
Tested on Next.js 16, Vercel hosting (other geo-aware hosts work with header tweaks).
When to Use This
- Routing first-time visitors to a locale-specific path
- Forcing pricing pages to show in the user's local currency
- Sending EU visitors through a GDPR consent flow before content
- Splitting traffic across regional CDN paths for compliance
Don't use this when the user has already chosen a locale (respect their cookie) or when search engines need to crawl every locale (geo-redirect can hurt international SEO if not paired with hreflang).
Code
// proxy.ts (project root)
import { NextResponse, type NextRequest } from 'next/server';
const SUPPORTED_LOCALES = ['us', 'in', 'uk', 'eu'];
const DEFAULT_LOCALE = 'us';
const LOCALE_COOKIE = 'preferred-locale';
const COUNTRY_TO_LOCALE: Record<string, string> = {
IN: 'in',
GB: 'uk',
DE: 'eu',
FR: 'eu',
ES: 'eu',
IT: 'eu',
US: 'us',
CA: 'us',
};
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
// Skip if URL already has a locale prefix
if (SUPPORTED_LOCALES.some((l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`))) {
return NextResponse.next();
}
// Respect existing user choice
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
return NextResponse.redirect(
new URL(`/${cookieLocale}${pathname}${search}`, request.url)
);
}
// Geo-detect from header (Vercel)
const country = request.headers.get('x-vercel-ip-country') ?? '';
const locale = COUNTRY_TO_LOCALE[country] ?? DEFAULT_LOCALE;
const response = NextResponse.redirect(
new URL(`/${locale}${pathname}${search}`, request.url)
);
// Persist the choice so we don't redirect again
response.cookies.set(LOCALE_COOKIE, locale, {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: 'lax',
});
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|api|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};The cookie check runs before the geo lookup, so a user who manually picks a locale never gets force-redirected again. The cookie also prevents an infinite redirect loop on the second request.
Usage
The locale switcher component that sets the cookie:
'use client';
import { useRouter } from 'next/navigation';
export function LocaleSwitcher() {
const router = useRouter();
function switchTo(locale: string) {
document.cookie = `preferred-locale=${locale}; path=/; max-age=31536000; samesite=lax`;
router.push(`/${locale}`);
}
return (
<select onChange={(e) => switchTo(e.target.value)} defaultValue="">
<option value="" disabled>Select region</option>
<option value="us">United States</option>
<option value="in">India</option>
<option value="uk">United Kingdom</option>
<option value="eu">Europe</option>
</select>
);
}Caveats
x-vercel-ip-countryonly exists on Vercel. For other platforms:cf-ipcountry(Cloudflare),CloudFront-Viewer-Country(CloudFront), or geo-IP API (slower, paid).- Headers can be spoofed in development. When testing locally, the header is empty, so you fall back to the default locale. Set it manually in DevTools to test other countries.
- Bot user agents bypass geo-redirect on most CDNs. Search engine crawlers often appear from US IPs regardless of the locale they're crawling for. Pair geo-redirect with
hreflangannotations so Google still indexes every locale. - Don't redirect on every request. The cookie check is what prevents this. Without it, every page load eats an extra HTTP round trip.
- Cookie path must be
/. Otherwise the cookie only applies to one path and you get redirect loops on other pages.
Related Snippets & Reading
- Auth-Gated proxy.ts — another proxy.ts pattern for session checks
- Goodbye middleware.ts, Hello proxy.ts — the deep dive on proxy.ts
- Vercel geo headers reference — every geo header Vercel injects
Frequently Asked Questions
Where does the country code come from in proxy.ts?
On Vercel, the country comes from the x-vercel-ip-country header that the edge network injects on every request. Other hosts use Cloudflare's cf-ipcountry, AWS CloudFront's CloudFront-Viewer-Country, or you can call a geo-IP API yourself. Vercel's value is the most reliable and free.
Should you geo-redirect or geo-render?
Redirect when the URL itself should change (locale prefix, country domain). Render variants when the URL stays the same but the content differs. Redirects are SEO-cleaner because each locale gets its own canonical URL, but they cost an extra round trip on first load.