How to Geo-Redirect Visitors by Country Using IP Geolocation
Geo-redirecting visitors to localized versions of your site is one of the most impactful things you can do for international users. A visitor from Germany landing on your English homepage might bounce โ but redirect them to your German page and they're far more likely to engage.
This guide covers three approaches: Next.js middleware (fastest), client-side JavaScript (simplest), and server-side (most flexible).
Approach 1: Next.js Middleware (Recommended)
If you're on Vercel, middleware runs at the edge before the page loads. This means zero flash of wrong content:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const COUNTRY_ROUTES: Record<string, string> = {
DE: "/de",
FR: "/fr",
ES: "/es",
JP: "/ja",
BR: "/pt",
};
export function middleware(request: NextRequest) {
// Skip if already on a localized path
const { pathname } = request.nextUrl;
if (pathname.startsWith("/de") || pathname.startsWith("/fr") ||
pathname.startsWith("/es") || pathname.startsWith("/ja") ||
pathname.startsWith("/pt")) {
return NextResponse.next();
}
// Skip if user has a locale preference cookie
if (request.cookies.get("locale-preference")) {
return NextResponse.next();
}
const country = request.headers.get("x-vercel-ip-country") || "";
const redirectPath = COUNTRY_ROUTES[country];
if (redirectPath && pathname === "/") {
const url = request.nextUrl.clone();
url.pathname = redirectPath;
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ["/"],
};Approach 2: Client-Side JavaScript
Works on any hosting provider. The tradeoff is a brief flash before redirect:
// Run this early in your page load
async function geoRedirect() {
// Don't redirect if user chose a locale manually
if (localStorage.getItem("locale-preference")) return;
try {
const { country } = await fetch(
"https://geo.kamero.ai/api/geo"
).then(r => r.json());
const routes = {
DE: "/de",
FR: "/fr",
ES: "/es",
JP: "/ja",
BR: "/pt",
};
const target = routes[country];
if (target && window.location.pathname === "/") {
window.location.href = target;
}
} catch {
// Silently fail โ user stays on default page
}
}
geoRedirect();Approach 3: Server-Side (Node.js / Express)
// Express middleware
app.use(async (req, res, next) => {
if (req.path !== "/" || req.cookies["locale-preference"]) {
return next();
}
try {
const response = await fetch("https://geo.kamero.ai/api/geo");
const { country } = await response.json();
const routes = { DE: "/de", FR: "/fr", ES: "/es" };
if (routes[country]) {
return res.redirect(302, routes[country]);
}
} catch {
// Fall through to default
}
next();
});Best Practices for Geo-Redirects
- Always let users override. Add a language/region picker and store the preference in a cookie. Never trap users in a locale they didn't choose.
- Use 302 (temporary) redirects, not 301. The same URL should serve different users differently, so search engines need to see the original URL.
- Don't redirect search engine bots. Check the User-Agent and skip redirects for Googlebot, Bingbot, etc. to avoid SEO issues.
- Set hreflang tags. Tell search engines about your localized pages so they can serve the right version in search results:
<link rel="alternate" hreflang="en" href="https://example.com/" />
<link rel="alternate" hreflang="de" href="https://example.com/de" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />Geo-Redirect vs Geo-Content
An alternative to redirecting is serving different content on the same URL based on location. This avoids the redirect latency and is simpler to implement:
// Server Component (Next.js on Vercel)
import { headers } from "next/headers";
const messages = {
DE: { greeting: "Willkommen!", cta: "Jetzt starten" },
FR: { greeting: "Bienvenue!", cta: "Commencer" },
JP: { greeting: "ใใใใ!", cta: "ๅงใใ" },
default: { greeting: "Welcome!", cta: "Get Started" },
};
export default async function Home() {
const h = await headers();
const country = h.get("x-vercel-ip-country") || "default";
const msg = messages[country] || messages.default;
return (
<main>
<h1>{msg.greeting}</h1>
<button>{msg.cta}</button>
</main>
);
}This approach works well for marketing pages where you want localized copy without maintaining separate URL structures.
Get Visitor Country Instantly
Free API, no key required. Works from any framework.
View Documentation โ