Edge Integration

Run PerfLocale behind a Cloudflare Worker, Vercel Edge function, or Netlify Edge function, and route visitors to the correct language before the request reaches PHP. Two pieces: a public JSON endpoint the edge reads once, and a header/cookie contract so edge decisions flow into WordPress.

Disabled by default. Enable via PerfLocale → Settings → Advanced → Edge Worker Integration, or programmatically with add_filter( 'perflocale/edge/enabled', '__return_true' ). When off, no REST route is registered and the edge_hint detection method is inert.

Config endpoint

URL: GET /wp-json/perflocale/v1/config
Auth: Public-read by default, gated by the perflocale/edge_worker/config_permission_callback filter. Default returns true because the payload contains only data already visible in the rendered HTML — see Non-sensitive invariant.
Cache headers: Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400 and an ETag so edges can revalidate with a cheap 304.

{
	"version": "1.0.0",
	"url_mode": "subdirectory",
	"url_prefix_type": "slug",
	"default_slug": "en",
	"hide_default_prefix": true,
	"excluded_paths": [ "/wp-json/", "/wp-admin/", "/wp-login.php" ],
	"detection_order": [ "url", "cookie", "browser", "default" ],
	"edge_hint_header": "X-PerfLocale-Lang",
	"edge_hint_cookie": "perflocale_edge_lang",
	"languages": [
		{
			"slug": "en",
			"locale": "en_US",
			"hreflang": "en-us",
			"prefix": "en",
			"domain": "",
			"text_direction": "ltr",
			"is_default": true
		}
	]
}

The payload is cached internally (3-layer cache, 15-minute TTL) and invalidated automatically whenever a language is added, updated, or removed, or when settings are saved.

Revalidation (ETag / If-None-Match)

Every 200 response carries ETag: "<md5>" derived from the JSON payload. Edges revalidate by sending If-None-Match on subsequent fetches; when the ETag matches, the endpoint returns 304 Not Modified with the same Cache-Control + ETag headers and an empty body — costing one md5 hash + a config-cache lookup. The ETag changes whenever the cached payload changes (language add/edit/remove, settings save, or any perflocale/api/config filter output change).

# First request
GET /wp-json/perflocale/v1/config
-> 200 OK
   ETag: "9f86d081884c7d659a2feaa0c55ad015"
   Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400

# Subsequent revalidation
GET /wp-json/perflocale/v1/config
If-None-Match: "9f86d081884c7d659a2feaa0c55ad015"
-> 304 Not Modified
   ETag: "9f86d081884c7d659a2feaa0c55ad015"
   Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400

Non-sensitive invariant

The config payload exposes only routing + language metadata: URL mode, prefix slugs, locales, hreflang values, text direction, default language, excluded paths, detection order, and the two edge-hint name constants. It never contains API keys, machine-translation provider tokens, user data, internal post/term IDs, capability lists, nonces, or anything that is not already observable from the rendered public site. This invariant is what justifies the public-read default. If you extend the payload via the perflocale/api/config filter, you must preserve it.

Restricting access

Sites that want to gate the endpoint (private staging, IP allowlist, mTLS proxy, Application-Password auth) filter perflocale/edge_worker/config_permission_callback. The default returns true. Return false or a WP_Error to produce a standard WP-REST 401/403. A non-bool / non-WP_Error return triggers _doing_it_wrong() (dev-mode) and the endpoint falls back to public-read rather than crashing.

// 1. Logged-in admins only (works with Application Passwords - the edge
//    worker presents Basic auth using an Application Password).
add_filter( 'perflocale/edge_worker/config_permission_callback', function () {
    return current_user_can( 'manage_options' );
} );

// 2. IP allowlist of known edge-worker egress addresses.
add_filter( 'perflocale/edge_worker/config_permission_callback', function () {
    $allowed = [ '203.0.113.10', '203.0.113.11' ];
    return in_array( $_SERVER['REMOTE_ADDR'] ?? '', $allowed, true );
} );

// 3. Application Password auth (force a real WP user behind the request).
//    Pair with an Authorization: Basic header from the edge worker.
add_filter( 'perflocale/edge_worker/config_permission_callback', function () {
    return is_user_logged_in()
        ? true
        : new WP_Error( 'rest_forbidden', 'Auth required', [ 'status' => 401 ] );
} );

Hint header + cookie

To have the edge’s decision honoured by PHP, add edge_hint to your detection order (Settings → URL & Routing → Redirect Priority, or via the perflocale/redirect/priority_order filter) and send one of:

  • Header: X-PerfLocale-Lang: fr - header wins over cookie
  • Cookie: perflocale_edge_lang=fr - fallback

Both names are filterable:

add_filter( 'perflocale/edge/hint_header', fn() => 'X-Vercel-Lang' );
add_filter( 'perflocale/edge/hint_cookie', fn() => 'my_lang_cookie' );

Per-request veto:

add_filter( 'perflocale/edge/accept_hint', function ( bool $accept, string $slug ): bool {
	// Reject hints from probes that don't come through our CDN.
	return ! empty( $_SERVER['HTTP_CF_RAY'] ) ? $accept : false;
}, 10, 2 );

Trust model

The hint is trusted at the same level as a visitor-set cookie: at worst it lets a visitor preselect a language they could have chosen themselves. URL-based detection always wins when a prefix is present in the request path, so a spoofed hint can never override a canonical translated URL.

Reference Cloudflare Worker

A complete example ships with the plugin at assets/js/edge-helper.js. It demonstrates both deployment strategies:

  • URL rewrite (recommended) - edge adds the language prefix and forwards; rewritten URL becomes the CDN cache key
  • Header hint - edge forwards unchanged with X-PerfLocale-Lang set; WordPress detects via edge_hint
const CONFIG_URL   = 'https://example.com/wp-json/perflocale/v1/config';
const CACHE_TTL_MS = 10 * 60 * 1000;

let cachedConfig    = null;
let cachedConfigExp = 0;

async function loadConfig() {
	if ( cachedConfig && Date.now() < cachedConfigExp ) return cachedConfig;

	const res = await fetch( CONFIG_URL, {
		cf: { cacheEverything: true, cacheTtl: 600 }
	} );
	cachedConfig    = await res.json();
	cachedConfigExp = Date.now() + CACHE_TTL_MS;
	return cachedConfig;
}

addEventListener( 'fetch', event => event.respondWith( handle( event.request ) ) );

async function handle( request ) {
	const config  = await loadConfig();
	const country = ( request.cf && request.cf.country ) || '';
	const slug    = pickLanguage( country, config ); // your logic

	if ( slug && slug !== config.default_slug ) {
		const url = new URL( request.url );
		url.pathname = '/' + slug + url.pathname;
		return fetch( new Request( url.toString(), request ) );
	}

	return fetch( request );
}

See the plugin file for the full version with Accept-Language fallback, excluded-path handling, and the header-hint alternative strategy.