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=86400Non-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-Langset; WordPress detects viaedge_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.
Related
- REST reference - full endpoint documentation
- Hooks reference —
perflocale/edge/*,perflocale/edge_worker/config_permission_callback, andperflocale/api/config - Cache-Tag headers - complements edge routing with language-scoped CDN purges