WordPressPHPGeolocationTutorial

WordPress IP Geolocation: Detect Country and Localize Content

ApogeoAPI6 min read

WordPress IP geolocation options

You have three paths to add geo detection in WordPress:

  1. Plugin (no code) — install wp-apogeo for shortcode-based geo content
  2. PHP in functions.php — call ApogeoAPI from server-side PHP
  3. JavaScript in the browser — detect country after page load

Option 1: wp-apogeo plugin (recommended)

The free wp-apogeo plugin wraps ApogeoAPI with WordPress transient caching (4h TTL) and exposes four shortcodes:

// In any post, page, or widget:
[apogeo_visitor_country]                    // outputs "Germany" for DE visitors
[apogeo_country iso="US"]                   // outputs "United States" (static)
[apogeo_exchange_rate currency="EUR"]       // outputs current EUR/USD rate
[apogeo_country_selector]                   // renders a <select> with all 250 countries

Install manually by uploading the plugin folder to wp-content/plugins/, then activate in the WordPress admin. Set your API key under Settings → ApogeoAPI.

Option 2: plain PHP in functions.php

Add a geo helper to your theme's functions.php:

<?php
// functions.php

function apogeo_get_visitor_geo(): array {
    static $cache = null;
    if ( $cache !== null ) return $cache;

    $api_key = defined( 'APOGEO_API_KEY' ) ? APOGEO_API_KEY : get_option( 'apogeo_api_key', '' );
    $transient_key = 'apogeo_geo_' . md5( $_SERVER['REMOTE_ADDR'] ?? '' );

    $cached = get_transient( $transient_key );
    if ( $cached !== false ) { $cache = $cached; return $cache; }

    $url = 'https://api.apogeoapi.com/v1/ip/me';
    $response = wp_remote_get( $url, [
        'headers'  => [ 'X-API-Key' => $api_key ],
        'timeout'  => 3,
        'blocking' => true,
    ]);

    if ( is_wp_error( $response ) ) {
        $cache = [ 'country_code' => 'US', 'country_name' => 'United States' ];
    } else {
        $data  = json_decode( wp_remote_retrieve_body( $response ), true );
        $cache = $data ?? [ 'country_code' => 'US', 'country_name' => 'United States' ];
        set_transient( $transient_key, $cache, 4 * HOUR_IN_SECONDS );
    }
    return $cache;
}

// Usage in any PHP template:
// $geo = apogeo_get_visitor_geo();
// $geo['country_code']  // "DE"
// $geo['country_name']  // "Germany"
// $geo['currency']      // "EUR"

Showing different content by country

Use the helper in any template or shortcode to conditionally render content:

// In a template file (e.g., page.php or block-template):
<?php
$geo   = apogeo_get_visitor_geo();
$cc    = $geo['country_code'] ?? 'US';
$eu    = [ 'DE','FR','IT','ES','NL','BE','AT','PT','SE','FI','DK','PL','CZ','RO','HU','BG','HR','SK','SI','EE','LV','LT','CY','LU','MT' ];
$is_eu = in_array( $cc, $eu, true );
?>

<?php if ( $is_eu ) : ?>
  <p>🇪🇺 VAT included in prices for EU customers.
     <a href="/privacy-gdpr">Privacy information (GDPR)</a></p>
<?php elseif ( $cc === 'US' ) : ?>
  <p>🇺🇸 Free shipping to the continental US on orders over $50.</p>
<?php else : ?>
  <p>🌍 Shipping available to <?php echo esc_html( $geo['country_name'] ?? 'your region' ); ?>.</p>
<?php endif; ?>

WooCommerce: display price in local currency

WooCommerce supports multi-currency through plugins (WooCommerce Payments, WOOCS, CURCY). For a simple display-only conversion without a plugin, use ApogeoAPI's exchange rates:

function apogeo_localized_price( float $usd_price ): string {
    $geo      = apogeo_get_visitor_geo();
    $currency = $geo['currency'] ?? 'USD';
    if ( $currency === 'USD' ) {
        return '$' . number_format( $usd_price, 2 );
    }
    // Fetch rate (cached separately for 4 h)
    $rate_key = 'apogeo_rate_' . $currency;
    $rate     = get_transient( $rate_key );
    if ( $rate === false ) {
        $api_key  = defined( 'APOGEO_API_KEY' ) ? APOGEO_API_KEY : get_option( 'apogeo_api_key', '' );
        $resp     = wp_remote_get(
            "https://api.apogeoapi.com/v1/exchange-rates/USD",
            [ 'headers' => [ 'X-API-Key' => $api_key ], 'timeout' => 3 ]
        );
        if ( ! is_wp_error( $resp ) ) {
            $data = json_decode( wp_remote_retrieve_body( $resp ), true );
            $rate = $data['rates'][ $currency ] ?? 1.0;
            set_transient( $rate_key, $rate, 4 * HOUR_IN_SECONDS );
        }
    }
    $local_price = round( $usd_price * (float) $rate, 2 );
    return $currency . ' ' . number_format( $local_price, 2 );
}

// Usage:
echo apogeo_localized_price( 29.00 ); // "EUR 26.80" for German visitors

Note: For actual checkout in local currency you need a proper multi-currency checkout extension. This pattern is for display-only localisation.

Option 3: JavaScript (browser-side)

If you can't modify PHP files (e.g., page builder sites), enqueue a small script:

// functions.php — enqueue the geo detection script
add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'apogeo-detect',
        plugin_dir_url( __FILE__ ) . 'js/geo-detect.js',
        [],
        '1.0.0',
        true  // load in footer
    );
    wp_localize_script( 'apogeo-detect', 'ApogeoConfig', [
        'apiKey' => defined( 'APOGEO_API_KEY' ) ? APOGEO_API_KEY : get_option( 'apogeo_api_key', '' ),
    ]);
});
// js/geo-detect.js
(async () => {
  const KEY = 'apogeo_geo';
  let geo = null;
  try {
    const raw = sessionStorage.getItem(KEY);
    geo = raw ? JSON.parse(raw) : null;
  } catch (_) {}

  if (!geo) {
    try {
      const res = await fetch('https://api.apogeoapi.com/v1/ip/me', {
        headers: { 'X-API-Key': ApogeoConfig.apiKey }
      });
      geo = await res.json();
      sessionStorage.setItem(KEY, JSON.stringify(geo));
    } catch (_) { return; }
  }

  // Show/hide elements with data-geo attributes
  document.querySelectorAll('[data-geo-show]').forEach(el => {
    const countries = el.dataset.geoShow.split(',');
    el.style.display = countries.includes(geo.country_code) ? '' : 'none';
  });
  document.querySelectorAll('[data-geo-hide]').forEach(el => {
    const countries = el.dataset.geoHide.split(',');
    el.style.display = countries.includes(geo.country_code) ? 'none' : '';
  });
})();

Then in any page builder you can add data-geo-show="DE,AT,CH" to show a block only to DACH visitors, or data-geo-hide="US" to hide something from US users.

Performance considerations

  • Transient caching: the PHP examples cache per-IP for 4 hours — a single cached API call serves all page views from that visitor for 4 hours.
  • Subnet caching: group IPs by first three octets (1.2.3) to share one cache entry across a /24 subnet (~250 users). Reduces API calls ~250×.
  • Fallback defaults: always provide a default country code ('US') if the API call fails, so your site works even during a network hiccup.
  • Free tier math: 1,000 req/month ÷ 30 days = ~33 unique IPs/day before caching. With /24 subnet caching you cover ~8,250 unique visitors/day on the free plan.

Try ApogeoAPI free

1,000 requests/month forever. 14-day full-access trial. No credit card.

Get your free API key