Edge delivery
Status. Edge delivery is package WP-57 (
integrations/edge, branchwp/WP-57-edge-delivery), not yet merged when this page was written. It was built and tested offline: in Node, in the real Cloudflare Workers runtime (workerd, through Miniflare) and in Chromium against the real loader. Nothing is deployed anywhere, no account was used, nothing is published. The Vercel, Netlify and Deno adapters are thin and untested on the real platforms (verify). The cost and limit figures below come from the package's README, which took them from Cloudflare's documentation on 2026-10-29; I did not fetch them again, so treat every figure as verify.
Edge delivery serves /<lang>/... (for example /fr/pricing) as real, translated HTML from your own hostname for sites that cannot be prerendered at build time: WordPress, server-rendered shops, any site behind a CDN that supports edge code. The edge fetches your original page, applies the signed bundle, sets lang and dir, a self-referencing canonical link and reciprocal hreflang, and returns it. Crawlers and link previews see translated text without running JavaScript.
When to use it#
| You have | Use |
|---|---|
| A static or framework site you build yourself | Build-time prerender: no runtime and no running cost |
| A site where you control the CDN or edge (Cloudflare, Vercel, Netlify, Deno) and want translated URLs for search engines | Edge delivery |
| No server-side control (Wix, Squarespace, Webflow, a script only) | The script snippet; read SEO and the script snippet first |
How a request is handled#
GET /fr/pricing
1. first path segment is a language prefix of the verified manifest? no -> your origin, untouched
2. fetch GET /pricing from your origin (full page, Accept-Language pinned to the source language)
3. verified bundle (signature, sha256, key epoch) -> translate with the extraction-spec engine
4. <html lang dir data-rl-ssr>, canonical (self), hreflang for every language + x-default, links /about -> /fr/about
5. weak ETag, capped Cache-Control, Content-Language, x-rl-edge header; cached per site key + bundle version + language + URL
ANY failure in 2-5 -> the origin page for /pricing as it is (source language), 200, marked and not cacheable
The language is decided by the URL only, never by Accept-Language or cookies, so there is no Vary: Accept-Language and no automatic redirect by language (Google advises against it).
What it will not break#
| Traffic | Behaviour |
|---|---|
Forms and POST, PUT, DELETE under /fr/... | Forwarded to the origin path with the client's own body, headers, cookies and Authorization. Same-site redirects get the prefix (/thanks becomes /fr/thanks); Set-Cookie passes through |
API calls, wp-admin, wp-json, api, checkout, cart, account, framework folders (_next, _nuxt, _astro), robots.txt, sitemap*.xml | Forwarded untouched (the default exclude list; RL_EXCLUDE replaces it) |
Assets and files (anything with an extension other than html, htm, php, asp, aspx, jsp), also with Range | Forwarded with the client's headers |
| Pages with cookies | Request cookies reach the origin; such a page is never cached by the edge |
| Conditional requests | The translated page has its own weak ETag, so If-None-Match gets 304. A Range request on a page URL gets the full 200 |
robots.txt | A path disallowed for * gets no translated copy. rl-edge robots generates the mirrored rules for the language folders |
noindex (X-Robots-Tag or a meta robots tag) | Translated with the same robots signals kept, but no canonical and no hreflang are added. RL_TRANSLATE_NOINDEX=false serves such pages untouched. Keep them out of your sitemap |
| Query strings | Translated, but no canonical or hreflang is added |
Fail open: reading the x-rl-edge header#
Every response the edge touches carries x-rl-edge. Anything other than translated means "served from your origin".
x-rl-edge | Meaning |
|---|---|
translated; lang=fr; v=12; cov=0.97 | Normal. v is the bundle version and cov the share of strings translated. ; cache=hit when served from the edge cache |
bypass; reason=no-bundle | No verified manifest (every bundle origin down or invalid, or a cold start). Set RL_LANGS so /fr/... still falls back to the source page instead of reaching your origin as an unknown URL |
reason=no-language-file | The language file is missing, or its SHA-256 or version does not match the signed manifest |
reason=robots, excluded, method, not-a-page, noindex | Deliberately untouched (see above) |
reason=coverage | The page is translated less than RL_MIN_COVERAGE |
reason=too-large, charset, fragment, unsupported-html, not-html, error | A page it cannot or should not translate: over RL_MAX_HTML_BYTES (2 MiB), not UTF-8, not a full document, or an unexpected exception |
Pages that could have been translated but were not also get Cache-Control: no-store and a Link: <source URL>; rel="canonical" header, so a source-language copy under a language URL is neither cached nor indexed as the translation. The only 5xx the edge can produce is 502 when your origin cannot be reached at all. A broken configuration turns the Worker into a plain pass-through. Rolling back means removing the route: source pages never depended on the Worker.
Trust#
The edge applies the same rules as the browser loader: manifest parse limits, the Ed25519 signature over the canonical manifest with your public key(s), key epochs, rollback refusal and the SHA-256 of every language file. There is no expiry, so a verified bundle keeps serving while the origins are down. A stale mirror serving an older version is skipped. Nothing secret is configured anywhere; never put an rl_sec_ key in the configuration.
Deploy on Cloudflare#
These are steps for you or the project owner; the repository never deploys. You need a Cloudflare account with your site's zone (Workers Paid recommended, see the limits below) and a published, signed bundle with your site key, public key and bundle origins.
cd integrations/edge && npm ci && npm run buildproducesdist/worker.js(about 64 KB gzipped including the HTML parser).- Copy
templates/wrangler.toml.templateto a deployment folder aswrangler.toml, copydist/worker.jsbeside it, fill the[vars](all public values) and theroutesfor your hostname. - Check the file offline:
node bin/rl-edge.mjs check-config wrangler.toml. - Optional: create a KV namespace and add the binding (a shared verified copy of the bundle, fewer origin fetches).
wrangler deployis your decision on your account. Then test:curl -sI https://www.example.com/fr/ | grep -i x-rl-edgeshould saytranslated.- Add
Sitemap: https://www.example.com/rl-sitemap.xmltorobots.txt, submit it in Search Console, and add the mirrored rules fromrl-edge robots --in robots.txt --lang fr,de.
Other platforms (verify each on the real platform): Vercel Edge Middleware, a Netlify Edge Function and Deno Deploy templates exist and use the same handler. On middleware platforms the origin page is fetched from the site itself with a loop-guard header; Deno Deploy needs RL_ORIGIN_BASE.
Configuration (all public values)#
| Variable | Default | Meaning |
|---|---|---|
RL_SITE_KEY | required | rl_site_live_... or rl_site_test_... |
RL_PUBLIC_KEYS | required | Ed25519 public key(s), base64url, space or comma separated, oldest first, optional kid=key |
RL_BASES | required | 1 to 5 https bundle origins, tried in order |
RL_SOURCE_LANG | en | Language of the original pages |
RL_SITE_URL | request origin | Public origin for canonical, hreflang and the sitemap |
RL_ORIGIN_BASE | request origin | Where original pages come from (required on Deno Deploy) |
RL_LANGS | none | Language codes known before the first bundle fetch (recommended) |
RL_HYDRATION | auto | auto strips framework hydration scripts on pages that hydrate; also ssr, loader, strip |
RL_MIN_COVERAGE | 0 | 0 to 1; below it the page is served untouched |
RL_REWRITE_LINKS | true | Root-relative links to page-like paths get the prefix; assets, files, forms and excluded areas do not |
RL_DECORATE_SOURCE | false | Add hreflang to source-language pages by streaming head injection. Off by default: the sitemap carries the reciprocal annotations and source pages stay byte-identical |
RL_EXCLUDE, RL_PRIVATE_COOKIES | see the package | Paths left untouched; cookies that make a request visitor-specific |
RL_CACHE_TTL, RL_MAX_STALE, RL_MANIFEST_TTL, RL_MAX_HTML_BYTES | 300, 300, 300, 2 MiB | Cache lifetime when the origin gives none; cap on cache lifetime; manifest refresh interval; page size cap |
RL_SITEMAP_PATH | /rl-sitemap.xml | Where the translated sitemap hint is served |
SEO helpers#
- Sitemap hint
GET /rl-sitemap.xmlis built from your/sitemap.xml(or a sitemap index, same host only, at most 20 children), without robots-disallowed and excluded paths. Every page and every translated copy lists all language versions asxhtml:linkalternates plusx-default. It is a hint: it assumes every page can be translated. - Reciprocity. hreflang only counts when it is reciprocal. Translated pages list all languages; source pages get theirs from the sitemap hint or from
RL_DECORATE_SOURCE=true. See Google's localized versions.
Limits and costs (verify all of this before quoting it)#
- CPU is the constraint. Translation parses the page twice with a pure-JavaScript HTML parser. The package's author measured in Node, on a development machine, about 70 to 190 ms for a 156 KB page (warm). Cloudflare's Workers Free plan allows 10 ms of CPU per request, so Free is not viable for uncached translated pages; Workers Paid allows 30 s by default. Cached pages, the normal case after the first request per language, URL, bundle version and data centre, cost almost no CPU. No real Cloudflare measurement has been done.
- Cloudflare prices quoted by the package: Workers Paid $5 a month minimum, 10 million requests included then $0.30 per extra million, 30 million CPU-milliseconds included then $0.02 per extra million; Free 100,000 requests a day; KV reads 10 million a month included then $0.50 per million, writes 1 million included then $5 per million. These may have changed.
- Subrequests: 50 on Free, 10,000 on Paid. A translated page needs one origin fetch (plus a cached
robots.txt). - After you publish a new bundle version, translated pages update within roughly 5 to 10 minutes (manifest refresh plus cache lifetime); purge the CDN cache to be faster.
- The origin must send UTF-8 pages that
fetchcan decode. An uncached/fr/request costs one origin request, so consider caching the HTML at the origin. - Translation is buffered, not streamed, and runs two parse passes per page. Real deployments on any platform, real CPU timings and real Search Console behaviour are not done. The origin's
Content-Security-Policyhashes and nonces for inline scripts are not recomputed (only relevant withstrip). JavaScript-rendered text is left to the browser loader where it runs.