Implementing Geo-Redirects: The Right and Wrong Ways

Redirecting users to a regional version of your site based on IP location. The patterns that work, the SEO traps, and the UX considerations.

Implementing Geo-Redirects: The Right and Wrong Ways

You have a multi-region site: example.com/us, example.com/uk, example.com/de. When a user from Germany lands on example.com, you want them to see the German version. The standard solution: geo-redirect, where you detect the user’s country from their IP and redirect to the regional URL.

This sounds simple. It’s not. Done poorly, geo-redirects hurt SEO, frustrate users who want a different region, and confuse caches. Done well, they’re invisible.

This post covers the patterns that actually work in 2026.

Why You Might Want Geo-Redirects

  • Language localization — Show users content in their language.
  • Currency / pricing — Show local currency and tax-included prices.
  • Regulatory compliance — Different terms / privacy notices per region.
  • Inventory / availability — Products that vary by region.
  • Content licensing — Show users content licensed in their country.

For some of these (language) a popup banner is better than a redirect. For others (currency, inventory) a redirect makes more sense.

The Simple (Wrong) Implementation

app.get('/', async (req, res) => {
    const country = await getCountry(req.ip)
    if (country === 'DE') return res.redirect('/de')
    if (country === 'FR') return res.redirect('/fr')
    return res.redirect('/us')  // default
})

This is the naive version. It “works” but has many problems:

  1. Search engines (Googlebot) get redirected based on the IP they’re crawling from — usually US. They never index /de or /fr.
  2. Users who want a different region can’t easily access it.
  3. Returning users always get redirected, even after they manually chose another region.
  4. Caching breaks — the cached version depends on the user’s IP.

Each problem has a fix; together they make geo-redirect more complex than it looks.

Fix 1: Honor User Choice

Once a user picks a region, remember it. Don’t keep redirecting them.

app.get('/', async (req, res) => {
    // Check cookie first
    const preferredRegion = req.cookies.region
    if (preferredRegion) {
        return res.redirect(`/${preferredRegion}`)
    }
    
    // Fall back to IP-based
    const country = await getCountry(req.ip)
    const region = countryToRegion(country)
    return res.redirect(`/${region}`)
})

When the user explicitly visits /de, set the cookie:

app.get('/de', (req, res, next) => {
    res.cookie('region', 'de', { maxAge: 365 * 24 * 60 * 60 * 1000 })
    next()
})

Now repeat visits remember the choice. A user from Germany who manually clicks “View US site” doesn’t keep getting redirected to /de.

Fix 2: Search Engine Handling

Search engines crawl from various IPs but want to index each regional version. Two approaches:

Option A: Don’t redirect crawlers

Detect search engine bots via User-Agent + verified reverse DNS (see bot detection). Serve content directly without redirect.

async function isVerifiedSearchBot(req: Request): Promise<boolean> {
    const ua = req.headers['user-agent'] ?? ''
    if (!/Googlebot|Bingbot/i.test(ua)) return false
    // Verify via reverse DNS
    const reverseDns = await lookupPtr(req.ip)
    return reverseDns.endsWith('.googlebot.com') || reverseDns.endsWith('.googlebot.com.')
}

app.get('/', async (req, res) => {
    if (await isVerifiedSearchBot(req)) {
        return serveDefaultContent(req, res)
    }
    // ... normal geo logic
})

Option B: Use hreflang

Tell search engines about regional versions via <link rel="alternate" hreflang>:

<link rel="alternate" hreflang="en-US" href="https://example.com/us" />
<link rel="alternate" hreflang="de-DE" href="https://example.com/de" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />

Each regional page includes hreflang for all variants. Search engines understand the relationship and index each version. With this in place, geo-redirects for normal users don’t hurt SEO.

Use both: hreflang for the global picture, and don’t redirect crawlers to avoid the situation where Googlebot can’t see your regional content.

Fix 3: Caching Considerations

If you cache / aggressively, you can’t serve different content per region. Options:

Don’t cache /

Make it dynamic; let the geo-redirect run for every request. Simple but defeats the cache.

Cache per-region

Use Vary or different cache keys per region. The CDN serves the cached redirect for each region.

Cache-Control: public, max-age=300
Vary: X-Country

Some CDNs (Cloudflare, Fastly) let you set cache keys based on geo headers.

Edge-side redirects

Do the redirect at the CDN/edge layer instead of origin. Cloudflare Workers, Vercel Edge Functions, AWS Lambda@Edge can all do this without hitting origin.

// Cloudflare Worker
export default {
    async fetch(request: Request) {
        const url = new URL(request.url)
        if (url.pathname === '/') {
            const country = request.headers.get('cf-ipcountry')
            const region = countryToRegion(country)
            return Response.redirect(`${url.origin}/${region}`, 302)
        }
        return fetch(request)
    }
}

This is fast and avoids origin requests entirely for redirects.

Fix 4: 302 vs 301

A subtle but important choice:

  • 301 Permanent Redirect — Caches forever. Search engines treat the destination as the canonical URL.
  • 302 Temporary Redirect — Doesn’t cache forever. Each request hits your server.

For geo-redirects: use 302. The redirect is context-dependent (changes per user); 301 would have terrible side effects (search engines indexing the wrong URL for the canonical, browsers caching the redirect indefinitely).

A Complete Pattern

Putting it together:

async function handleRoot(req: Request, res: Response) {
    // 1. Search engine: serve content directly with hreflang
    if (await isVerifiedSearchBot(req)) {
        return serveDefaultWithHreflang(req, res)
    }
    
    // 2. Honor user's explicit choice from cookie
    const cookieRegion = req.cookies?.region
    if (cookieRegion && SUPPORTED_REGIONS.has(cookieRegion)) {
        return res.redirect(302, `/${cookieRegion}`)
    }
    
    // 3. Geo-detect by IP
    const country = req.headers['cf-ipcountry'] ?? await getCountry(req.ip)
    const region = countryToRegion(country) ?? 'us'  // default to /us
    
    // 4. Redirect (302 since geo is context-dependent)
    return res.redirect(302, `/${region}`)
}

// Set region cookie when user explicitly visits a region
app.get('/:region/*', (req, res, next) => {
    if (SUPPORTED_REGIONS.has(req.params.region)) {
        res.cookie('region', req.params.region, { 
            maxAge: 365 * 24 * 60 * 60 * 1000,
            sameSite: 'lax'
        })
    }
    next()
})

This handles:

  • Crawlers correctly.
  • Returning users with explicit preferences.
  • New users via geo.
  • Sensible defaults.

UX Considerations

A few things to get right beyond the technical:

Show a region switcher

Even with auto-detection, every page should have a clear “switch region” UI. Users in Germany on business travel may want the US version.

Don’t be aggressive

Some sites force users back to the auto-detected region. Frustrating for legitimate use cases. Users who explicitly clicked a region should stay there.

Announce the redirect (sometimes)

A banner like “We detected you’re in Germany. View our German site?” is friendlier than silent redirect for the first visit.

Mobile considerations

Mobile users on roaming may have IPs from other countries. See mobile network architecture. Your auto-detected region might be wrong; the switcher is essential.

VPN users

A VPN user is intentionally in a different region. Respect their choice; don’t fight them.

Hreflang in Depth

A few details for hreflang:

  • Each region’s page should list all variants including itself.
  • Use ISO 639-1 + ISO 3166-1 codes: en-US, de-DE, pt-BR.
  • x-default is the global / no-specific-region fallback.
  • Bidirectional/us says it has /de; /de says it has /us. Both directions for search engines to validate.

This is a separate (but related) SEO concern from geo-redirect logic. Both should be in place.

Geo-Restriction (Hard Block) vs Geo-Redirect

A note on the difference:

  • Geo-redirect — User goes to the right page automatically. Friendly. Reversible.
  • Geo-restriction — User is blocked entirely from some regions. Hard. See geofencing 101.

Streaming services often use restriction (license-driven). Most other use cases want redirect, not restriction.

Using the IP API

For detecting country, you can use:

  • CDN headers (CF-IPCountry, CloudFront-Viewer-Country) if you’re behind a CDN. Zero-latency, no API call.
  • Hosted API like Ip2Geo if you’re not behind a CDN or want richer data.
  • Local database (GeoLite2 or commercial) for low-latency on-premise.

For details, see detect country from IP.

TL;DR

  • Geo-redirect sends users to a regional URL based on IP-derived country.
  • Use 302, not 301. Geo is context-dependent.
  • Honor user cookies for explicit region choices.
  • Handle search engines separately (verified bot detection + hreflang).
  • Implement region switcher UI for users who need to override.
  • Edge-side redirects (Cloudflare Workers, etc.) are fast and cache-friendly.
  • hreflang tags help search engines index each regional version.
  • VPN users and roaming users make IP-based geo imperfect — the switcher fixes this.

Geo-redirects look like a simple “if country = X, redirect to Y” but production-quality implementations handle multiple concerns (SEO, returning users, caches, user choice). For a deeper look at geographic personalization patterns, see geo personalization; for hard regional access controls, geofencing 101.

Get Started

Convert IPs into accurate location data in milliseconds.

Sign up today and get 1,000 free monthly stored conversions, and discover why developers trust us for fast, reliable, and affordable IP conversions.