---
title: "Edge Integration - Cloudflare Workers and CDN - PerfLocale"
description: "Pre-route visitors at the edge - Cloudflare Workers, Vercel Edge, Netlify Edge. Public config endpoint + hint-header contract."
canonical: "https://perflocale.com/docs/edge-integration/"
source: "https://perflocale.com/docs/edge-integration/"
format: "markdown"
---
# 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`](#restricting-access) filter. Default returns `true` because the payload contains only data already visible in the rendered HTML — see [Non-sensitive invariant](#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.

```json
{
	"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.

### Query-parameter mode

The path-prefix rewrite (the “URL rewrite” strategy below) assumes a _path-based_ mode. When `url_mode` is `"query"`, PerfLocale routes non-default languages by a `?lang=<slug>` query parameter and the default language always stays on clean URLs (no param); it never uses a path prefix. Note `languages[].prefix` is still the plain slug in query mode (the prefix field ignores `url_mode`), so a worker that blindly prepends `'/'+prefix` produces URLs like `/de/page/` that query mode 404s, and the “already prefixed” guard never matches. In query mode the edge worker should either **(a)** append `?lang=<slug>` to the forwarded URL for non-default languages instead of rewriting the path, or **(b)** use the “Header hint” strategy (forward unchanged with `X-PerfLocale-Lang` set), which is mode-agnostic. Gate any path rewrite on `config.url_mode` so it only fires in a path-based mode.

### 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`](https://perflocale.com/docs/hooks/#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.

```php
// 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`](https://perflocale.com/docs/hooks/#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:

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

Per-request veto:

```php
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.

## Related

-   [REST reference](https://perflocale.com/docs/rest-api/#edge-config) - full endpoint documentation
-   [Hooks reference](https://perflocale.com/docs/hooks/#edge-integration) — `perflocale/edge/*`, [`perflocale/edge_worker/config_permission_callback`](https://perflocale.com/docs/hooks/#perflocale-edge-worker-config-permission-callback), and [`perflocale/api/config`](https://perflocale.com/docs/hooks/#perflocale-api-config)
-   [Cache-Tag headers](https://perflocale.com/docs/cache-tags/) - complements edge routing with language-scoped CDN purges
