DEV Community

Vix
Vix

Posted on

Building a country-based redirect — the right way (and the wrong ways)

Building a country-based redirect — the right way (and the wrong ways)

Redirecting users based on their country is one of those features that seems trivial until you actually build it. Get it wrong and you'll trap users in the wrong locale, break back buttons, annoy people on VPNs, and tank your SEO. This article covers a working implementation, the common mistakes, and when you shouldn't redirect at all.


What "country-based redirect" usually means

A visitor lands on example.com, and based on their detected location, you either:

  • Redirect them to a localized subdomain/path (example.comexample.com/de)
  • Redirect them to a country-specific TLD (example.comexample.de)
  • Show a banner suggesting a redirect, without forcing it
  • Adjust content in place without any URL change (no redirect at all)

The last two options are usually the better choice, and we'll get to why. First, the mechanics.


Basic implementation

Using IPPubblico.org to detect the country server-side and redirect:

// Node.js / Express
app.get('/', async (req, res) => {
  const forwarded = req.headers['x-forwarded-for'];
  const clientIP  = forwarded ? forwarded.split(',')[0].trim() : req.socket.remoteAddress;

  try {
    const geoRes = await fetch(`https://ippubblico.org/?api=1`, {
      signal: AbortSignal.timeout(3000)
    });
    const data = await geoRes.json();
    const countryCode = data.geo?.country_code;

    const routes = { DE: '/de', FR: '/fr', JP: '/ja', BR: '/pt-br' };
    if (routes[countryCode] && req.path === '/') {
      return res.redirect(302, routes[countryCode]);
    }
  } catch {
    // Geolocation failed — fall through to default
  }

  res.redirect('/en');
});
Enter fullscreen mode Exit fullscreen mode
# Flask
from flask import request, redirect
import requests

ROUTES = {'DE': '/de', 'FR': '/fr', 'JP': '/ja', 'BR': '/pt-br'}

@app.route('/')
def index():
    try:
        data = requests.get('https://ippubblico.org/?api=1', timeout=3).json()
        country = data.get('geo', {}).get('country_code')
        if country in ROUTES:
            return redirect(ROUTES[country], code=302)
    except requests.RequestException:
        pass
    return redirect('/en', code=302)
Enter fullscreen mode Exit fullscreen mode

This works, but it's also the version most likely to cause problems. Let's go through why.


Mistake 1 — using a permanent (301) redirect

// Don't do this
res.redirect(301, routes[countryCode]);
Enter fullscreen mode Exit fullscreen mode

A 301 tells browsers and search engines "this redirect is permanent, cache it forever." If your geolocation is ever wrong for a user — VPN, CGNAT, traveling — that user's browser will cache the wrong redirect and keep sending them to the wrong locale even after the underlying condition changes.

Use 302 (temporary redirect) for anything based on IP geolocation. It should never be permanent, because IP-to-location mapping isn't permanent either.


Mistake 2 — redirecting on every page, not just the homepage

// Don't do this — redirects on every route
app.use(async (req, res, next) => {
  const country = await detectCountry(req);
  if (shouldRedirect(country, req.path)) {
    return res.redirect(302, buildLocalizedPath(req.path, country));
  }
  next();
});
Enter fullscreen mode Exit fullscreen mode

If you redirect based on geolocation on every request, you break deep links. Someone shares example.com/pricing, and every visitor gets redirected to their own locale's pricing page, even if they wanted to see the original page a specific person linked to.

Detect and redirect once, typically only from the root path, and respect whatever locale the user is already on for every other page.


Mistake 3 — no way to override

This is the most common complaint about geolocation redirects, and for good reason. A French user living in Japan, a VPN user, someone testing your site from a different country — all get stuck.

app.get('/', async (req, res) => {
  // Respect explicit user choice first
  const savedLocale = req.cookies.locale;
  if (savedLocale) {
    return res.redirect(302, `/${savedLocale}`);
  }

  // Only geolocate if there's no saved preference
  const country = await detectCountry(req);
  const locale  = LOCALE_MAP[country] ?? 'en';
  res.redirect(302, `/${locale}`);
});

app.get('/set-locale/:locale', (req, res) => {
  res.cookie('locale', req.params.locale, { maxAge: 365 * 24 * 60 * 60 * 1000 });
  res.redirect(req.query.return ?? '/');
});
Enter fullscreen mode Exit fullscreen mode

Always provide a visible language/region switcher on every page, and always let a saved cookie override geolocation on subsequent visits.


Mistake 4 — redirecting VPN and cloud users incorrectly

VPN exit nodes and cloud provider IPs geolocate to wherever the server physically sits, not where the user is. Redirecting these users based on IP alone routes them somewhere unexpected.

const CLOUD_ASNS = new Set([
  'AS16509', // AWS
  'AS15169', // Google Cloud
  'AS8075',  // Azure
  'AS14061', // DigitalOcean
]);

async function shouldSkipRedirect(ip) {
  try {
    const data = await fetch(`https://ippubblico.org/?api=1`).then(r => r.json());
    return CLOUD_ASNS.has(data.asn);
  } catch {
    return true; // fail safe — skip redirect if detection fails
  }
}
Enter fullscreen mode Exit fullscreen mode

If the ASN belongs to a known cloud/VPN provider, skip the automatic redirect and default to English (or your primary locale) instead.


Mistake 5 — hurting SEO with redirect chains

Search engine crawlers are typically routed through US or EU datacenter IPs. If your redirect logic sends Googlebot to /en every time regardless of what page it's crawling, you can end up with every URL on your site resolving to the same redirected page in search console — collapsing your site's indexed pages into one.

const BOT_USER_AGENTS = /googlebot|bingbot|yandex|baiduspider|duckduckbot/i;

app.get('/', async (req, res) => {
  if (BOT_USER_AGENTS.test(req.headers['user-agent'] ?? '')) {
    return next(); // let crawlers see the default page, don't redirect them
  }
  // ... normal redirect logic
});
Enter fullscreen mode Exit fullscreen mode

Always exclude known crawler user agents from geolocation redirects. Let them index your canonical/default version, and use hreflang tags to tell search engines about your other language versions instead of redirect-based discovery.


A better alternative — suggest, don't force

For most sites, a non-intrusive banner beats a hard redirect:

function LocaleBanner() {
  const [suggestion, setSuggestion] = useState(null);

  useEffect(() => {
    if (document.cookie.includes('locale_dismissed=1')) return;

    fetch('https://ippubblico.org/?api=1')
      .then(res => res.json())
      .then(data => {
        const code = data.geo?.country_code;
        const suggested = LOCALE_MAP[code];
        const current = document.documentElement.lang;
        if (suggested && suggested !== current) {
          setSuggestion({ code, locale: suggested });
        }
      })
      .catch(() => {});
  }, []);

  if (!suggestion) return null;

  return (
    <div className="locale-banner">
      <p>{`Would you like to view this page in ${suggestion.locale}?`}</p>
      <a href={`/${suggestion.locale}`}>Yes, switch</a>
      <button onClick={() => {
        document.cookie = 'locale_dismissed=1; max-age=31536000';
        setSuggestion(null);
      }}>
        No, stay here
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This never breaks a shared link, never confuses a crawler, and respects a user's explicit choice permanently once dismissed.


When not to redirect at all

  • Single-language sites with regional pricing only — adjust price/currency display, don't change the URL or content structure
  • Documentation sites — deep links matter enormously here; a redirect that breaks a bookmarked doc page is actively harmful
  • API-first products — API consumers are developers, not casual browsers; geolocation-based redirects add friction with no benefit
  • Sites where SEO matters more than personalization — every redirect is a small SEO risk; if your primary traffic is organic search, weigh that against the personalization benefit

Quick reference

Approach Use when
Hard redirect (302, cookie override, bot exclusion) Multi-region product with genuinely different content per locale
Suggestion banner Most content sites — safer default
No redirect, adjust content in place Pricing/currency only, single-language UI
No geolocation at all API products, documentation, anything link-sensitive

Conclusion

A country redirect done carelessly causes more problems than it solves — broken back buttons, cached wrong locales, confused VPN users, and collapsed SEO. Done carefully, with a 302, a cookie override, bot exclusion, and ASN-based cloud/VPN detection, it's a reasonable enhancement for genuinely multi-region products.

For most sites, a dismissible suggestion banner gets 80% of the benefit with a fraction of the risk.


Have you built (or been burned by) a country redirect? The comments are a good place for war stories.

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

Did you explore using edge functions or CDN rules for the "right way" approach, and if so,