Geo-Based IP Redirect with Cloudflare and WordPress: redirect to another domain by country without plugins

You have a WooCommerce store serving customers from Bulgaria and a second domain that sells in Romania. Bulgarian users see the Bulgarian domain and you want to drive Romanian users to the Romanian site.

The problem: WordPress needs to understand where each visitor is coming from before it loads anything. Most geo-redirect plugins do just that – execute PHP logic on every request, kill the page cache and add 200-400ms of latency.

How does a Romanian user automatically get to a Romanian domain where everything is in the language and currency they expect? We’ll explore two approaches that work well for this task – one requires Cloudflare Workers, the other is a simple PHP snippet directly in WordPress.

Why geo-redirect plugins slow down WooCommerce

Most WordPress plugins for geo-redirect (IP2Location, GeoTargetingWP, Redirection with geo rules) work at PHP level. With each request, WordPress loads the entire stack – wp-load.php, wp-settings.php, active plugins, theme functions – just to check the IP address and decide whether to redirect.

This behavior makes page caching almost impossible. If Varnish or LiteSpeed Cache returns a cached Bulgarian page to a Romanian user, the result is wrong. Therefore, these plugins usually add a DONOTCACHEPAGE constant or exclude certain URLs from the cache. TTFB jumps from 150ms to 600-800ms on each first load because each request goes through the full PHP stack. For a shop that relies on good Core Web Vitals results, this is unacceptable.

The two approaches below avoid these plugins entirely.

Option 1: PHP snippet with CF-IPCountry header

This is the simplest workable option. Cloudflare automatically adds the CF-IPCountry header to every request that passes through the network. The only condition – the domain must be behind the Cloudflare proxy (the orange cloud in DNS settings). The basic Cloudflare configuration takes 10 minutes.

The PHP code is placed in the functions.php of the child theme or in the Code Snippets plugin of the Bulgarian domain:

add_action('template_redirect', function() {
    if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
        return;
    }

    if (defined('REST_REQUEST') && REST_REQUEST) {
        return;
    }

    $country = strtoupper(
        sanitize_text_field($_SERVER['HTTP_CF_IPCOUNTRY'] ?? '')
    );

    if ($country === 'RO') {
        wp_redirect('https://example.ro', 302);
        exit;
    }
}, 1);

The priority 1 in add_action is essential. WordPress executes the template_redirect hooks in order of priority – a value of 1 means the redirect happens before the theme, before WooCommerce (priority 10+), before the analytics plugins. Less PHP code is executed, faster the redirect comes.

What the code checks before redirecting

is_admin() stops execution for /wp-admin/ requests. wp_doing_ajax() prevents AJAX calls from the WooCommerce cart and checkout forms. wp_doing_cron() disables WP-Cron tasks. The REST_REQUEST constant protects REST API endpoints that may use other plugins or mobile apps.

Without these checks, redirect can break the admin panel when accessed from a Romanian IP, break AJAX adds to the cart, or block API integrations.

Limitations of the PHP approach

The PHP snippet runs WordPress partially before the redirect. template_redirect is triggered after wp-load.php and wp-settings.php, which means 50-150ms PHP overhead even on the fastest hosting. For most sites this is perfectly acceptable – the user sees the redirect in under 200ms.

The more serious problem is caching. If the server uses full-page caching (Varnish, LiteSpeed, nginx FastCGI), the cached page is served before PHP is triggered. A Romanian user gets the cached Bulgarian HTML instead of the redirect. The solution is to add a Cache-Vary header by CF-IPCountry or exclude redirect pages from the cache.

add_action('send_headers', function() {
    if (!is_admin()) {
        header('Vary: CF-IPCountry', false);
    }
});

This Vary header tells the cache layer that the response depends on the state of the user. LiteSpeed and Varnish will keep separate cache copies for BG and RO traffic. The downside: the cache gets bigger and less efficient because each page is stored in multiple versions.

Option 2: Cloudflare Workers – redirect at edge level

Cloudflare Workers execute JavaScript on the edge server closest to the user before the request reaches WordPress. There is no PHP overhead. No conflict with page cache. Redirect happens in under 5ms.

Workers’ free plan includes 100,000 requests per day – enough for most small and medium-sized stores.

Create a Worker

In the Cloudflare Dashboard, Workers & Pages section, click Create Application and then Create Worker. Give a descriptive name – for example geo-redirect-ro. Cloudflare generates a preview URL, but for production the Worker will connect to the route of the Bulgarian domain.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const country = request.headers.get('CF-IPCountry') || 'XX';
    const path = url.pathname;

    // Skip assets, admin, API, AJAX
    if (
      path.startsWith('/wp-admin') ||
      path.startsWith('/wp-json') ||
      path.startsWith('/wp-content') ||
      path.match(/.(js|css|png|jpg|webp|svg|woff2?)$/) ||
      url.searchParams.has('wc-ajax')
    ) {
      return fetch(request);
    }

    // Romanian visitors -> Romanian domain
    if (country === 'RO') {
      return Response.redirect('https://example.ro' + path, 302);
    }

    return fetch(request);
  }
};

The 301 would be cached by the browser and a user traveling between countries would see the wrong domain with no way to correct themselves.

Route configuration

After deploy, in the Triggers section add the route: example.bg/*. The worker will be triggered for all requests to the Bulgarian domain. The wc-ajax check in the code prevents the WooCommerce AJAX cart from redirecting mid-checkout.

For WooCommerce webhooks and API integrations with ERP or accounting software, add additional exceptions. Webhook URLs usually go through /wp-json/wc/v3/, which is already covered, but custom endpoints may require a separate check.

Automatic redirection creates a problem: a Romanian user who wants to order from the Bulgarian store (lower prices, different product catalog) cannot stay on the .bg domain. IP-based geolocation is not 100% accurate – VPN users, corporate networks and mobile operators routing through another country will get the wrong redirect.

The cookie override mechanism is mandatory.

For the PHP version, add a check at the beginning of the snippet:

add_action('template_redirect', function() {
    if (isset($_COOKIE['skip_geo_redirect'])) {
        return;
    }

    if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
        return;
    }

    if (defined('REST_REQUEST') && REST_REQUEST) {
        return;
    }

    $country = strtoupper(
        sanitize_text_field($_SERVER['HTTP_CF_IPCOUNTRY'] ?? '')
    );

    if ($country === 'RO') {
        wp_redirect('https://example.ro', 302);
        exit;
    }
}, 1);

On the frontend, language switcher or banner “Want to stay on the Bulgarian site?” sets skip_geo_redirect cookie with JavaScript:

document.cookie = 'skip_geo_redirect=1; max-age=2592000; path=/; SameSite=Lax';

30 days (2592000 seconds) is a reasonable balance – long enough not to be annoying, short enough not to be permanent.

For the Workers variant, add a cookie check before the redirect logic:

const cookies = request.headers.get('Cookie') || '';
if (cookies.includes('skip_geo_redirect=1')) {
  return fetch(request);
}

SEO: hreflang and indexing with two domains

Google should know that .bg and .ro domains are language variants of a business, not duplicate content. In the head section of each page of both domains:

Googlebot usually identifies itself with CF-IPCountry:US. If neither the PHP snippet nor the Worker redirects US traffic (and it shouldn’t), Google will index the Bulgarian domain normally. The Romanian domain will be indexed independently by Google Romania.

A 302 redirect is the correct code for a geo-based redirect. A 301 tells Google “this page has moved forever”, which is not true – the page exists on both domains.

PHP snippet or Workers: when which option

The PHP option is suitable when the site is already behind Cloudflare, the hosting does not have an aggressive full-page cache and the traffic is moderate – up to 50 000 visits per month. Maintenance is simple: a snippet in functions.php or the Code Snippets plugin that every WordPress developer understands.

Workers option is better for shops with high traffic, aggressive caching (Varnish, LiteSpeed Enterprise, Cloudflare APO) and need for zero PHP overhead. The redirect happens before the origin server knows there is a request. For sites where every millisecond counts, edge redirect is the way to go.

Both options work with WebP image optimization and other front-end optimizations without conflict. The geo-redirect logic is completely independent of the rendering pipeline.

Testing without travelling to Romania

Curl with custom header tests the PHP variant directly:

curl -I -H "CF-IPCountry: RO" https://example.bg/

The response expected under HTTP 302 is https://example.ro. If you get 200 and HTML content instead, the page cache serves before PHP. Clear the cache and test again, or add the Vary header from the constraints section.

VPN from Romania gives a full end-to-end test – DNS resolution, Cloudflare edge routing, WordPress redirect. Windscribe and ProtonVPN have free Romanian servers.

For the Workers variant, Cloudflare Dashboard displays real-time logs in the Workers > Logs section. Each redirect is visible with country code, URL and HTTP status.

Frequently Asked Questions

  1. Does geo-redirect work without Cloudflare?

    The PHP snippet relies on the CF-IPCountry header, which only adds Cloudflare. Without Cloudflare, an alternate geo-IP database is required (MaxMind GeoLite2), which adds additional complexity and query to the database on each load.

  2. 302 or 301 redirect by country?

    Always 302 (temporary). 301 is cached by the browser and tells Google that the page has moved permanently. With geo-redirect, both URLs are valid – they just serve different audiences.

  3. How much does Cloudflare Workers cost for geo-redirect?

    The free plan includes 100,000 requests per day. For most small and medium shops, this is enough. The paid plan is $5/month for 10 million queries.

  4. What happens to a Romanian consumer who wants to buy from the Bulgarian shop?

    Cookie override mechanism allows manual override. Banner or button sets skip_geo_redirect cookie and redirect stops for 30 days.

  5. Will geo-redirect break the WooCommerce cart?

    Not if AJAX requests are excluded from the redirect logic. The PHP snippet checks wp_doing_ajax(), and the Workers variant filters out the wc-ajax parameter.

Share: