INTERMEDIATENEXTJSMIDDLEWARE

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.

Published May 12, 2026
nextjsproxy-tsgeoi18nredirect

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-country only 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 hreflang annotations 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.

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.

X (Twitter)LinkedIn