NORG Edge Router for WordPress
Plugin 0.1.3. The install steps are on the plugin guide; the architecture it sits in is on the solution architecture page; the one rule WP Engine needs is on its own page; the latency it adds, measured on Norg’s own test site, is on the performance results page. Everything here is read from the plugin source for this version, and the source is available on request before you install it.
Request lifecycle
The plugin hooks plugins_loaded at priority −1000, before any other plugin, and wraps its whole run in a catch: an exception is logged and the page is served normally.
- Any request while disabled, or carrying the
x-norg-edgeloop-guard header or anupgradeheader. - Admin, AJAX, cron, WP-CLI and XML-RPC requests; WordPress internal paths and
rest_routerequests. - Anything but GET, except HEAD on NORG’s own artefacts and POST to the MCP path.
- A marked request (
?norg_edge) is made uncacheable, whatever follows. - Traditional search crawlers (googlebot, bingbot, applebot, duckduckbot, baiduspider, yandex, slurp) are passed through. This list is a constant; no setting reaches it.
- The user agent is matched against the policy feed by lower-cased substring; the first match wins.
- The matched pattern’s serving policy decides what happens, as below.
Classification and serving
The plugin ships no agent list of its own. Patterns, their serving policy and the operators’ address ranges all arrive in the policy feed; with no feed there is no behaviour.
| Feed policy | What the plugin does |
|---|---|
| never_divert | Serve the normal page. Scripts and HTTP libraries (headless browsers, curl, Python clients) are always in this class. |
| divert | Check the source address against the operator’s published ranges; serve the mirror only if it matches. The check is address-range only and fails closed. |
| divert_unverified | Serve the mirror without the address check, for an operator that publishes no ranges. Only ever returns an existing mirror; never triggers a render. |
| ignore | Neither divert nor count as crawler traffic (opt-out signals such as Google-Extended). |
Ranges are published by the operators and refreshed by NORG daily (OpenAI’s and Anthropic’s JSON files, Perplexity’s two files, Meta via its autonomous system). Which address is checked depends on NORG_TRUSTED_PROXY: auto takes CF-Connecting-IP from a Cloudflare peer, the last X-Forwarded-For or X-Real-IP hop from a private or shared-space proxy, else REMOTE_ADDR. A query string ?agent=true forces the mirror for a preview, if one exists.
A 200 from the content service is served. A definite 404 triggers a render request and the strip fallback: the live page is captured in an outer output buffer, scripts, styles, navigation and other chrome are removed, and the result is served with X-Norg-Edge: stripped if at least 120 words remain; otherwise the untouched page is served. A transport error, timeout or any other status serves the untouched page.
Headers and page caches
Every mirror or stripped response is marked uncacheable in each dialect a cache might read, and the plugin defines the shared constants cache plugins honour.
| Header | On a mirror or stripped response |
|---|---|
| Cache-Control | private, no-store |
| CDN-Cache-Control | private, no-store |
| X-LiteSpeed-Cache-Control | no-cache |
| X-Accel-Expires | 0 |
| X-Norg-Edge | mirror, or stripped |
| X-Norg-Edge-Env | production or test |
| Set-Cookie, Expires, Pragma, Last-Modified, Link, X-Pingback | Removed. |
On a mirror response the plugin defines DONOTCACHEPAGE, DONOTCACHEOBJECT, DONOTMINIFY and DONOTROCKETOPTIMIZE, and calls LiteSpeed’s no-cache hook. For agents to reach it in the first place, it registers the agent user agents with WP Rocket, LiteSpeed Cache, WP Super Cache, WP Fastest Cache, W3 Total Cache and FlyingPress when it detects them (the page caches section lists each mechanism). Caches that run before PHP at the host need a host rule; WP Engine’s is one internal rewrite. Mirror HTML is deliberately not marked noindex. NORG’s shared assets under /.norg/ are served cacheable for 60 s.
Settings and wp-config constants
Settings live in one option, norg_edge_settings, which is not autoloaded. A constant defined in wp-config.php always wins over the saved value and locks that field on the settings screen, which is how a host or agency pins a site’s configuration.
| wp-config constant | Default | Purpose |
|---|---|---|
| NORG_SITE_ID | (none) | The site’s UUID in NORG. Required. |
| NORG_SITE_KEY | (none) | The site key (nek_live_… or nek_test_…). Defined here, it is never written to the database. |
| NORG_API_URL | https://content-craft-api.norg.ai | Control-plane host. |
| NORG_CONTENT_BASE | https://edge-content.norg.ai | Content-service host the mirrors are fetched from. |
| NORG_EDGE_ENV | derived from the API host | production or test; echoed in the X-Norg-Edge-Env response header. |
| NORG_EDGE_DISABLED | false | Kill switch. Every visitor gets the normal site, effective on the next request. |
| NORG_CAPTURE_ONLY | false | Record-only mode: report agent visits (and citation page-views), serve nothing different. |
| NORG_STRIP_FALLBACK_ENABLED | true | When no mirror exists, serve the live page stripped to text (120-word floor). |
| NORG_LAZY_RENDER_ENABLED | true | Ask NORG to render a page the first time a verified agent asks for it. |
| NORG_TRUSTED_PROXY | auto | Where the visitor address is read from: auto, none, cloudflare, or header. |
| NORG_TRUSTED_PROXY_HEADER | (none) | The header to read when NORG_TRUSTED_PROXY is header. |
| NORG_EDGE_ORIGIN_PROBE | true | Answer NORG’s keyed health probe (x-norg-edge-check). |
| NORG_EDGE_CACHE_BYPASS | true | Register the agent user agents with detected cache plugins. |
| NORG_EDGE_HTACCESS_BLOCK | false | Write a generic .htaccess bypass block for Apache/LiteSpeed caches with no API. |
| NORG_EDGE_AUTO_UPDATE | true | Install plugin updates from NORG automatically. false means each update is approved by hand. |
| NORG_EDGE_DEBUG | false | Write a debug log to the PHP error log. The site key is never logged. |
Where the Site Key lives
The Site ID must be a UUID and the key must match nek_live_… or nek_test_…. NORG stores only the key’s SHA-256 hash, so it cannot be recovered from NORG; what follows is how the plugin holds your copy.
The key is read from the constant on each request and never written to the database. The settings screen shows the field as locked, and the REST connect endpoint answers 409 rather than overwrite it.
The key is encrypted before it is saved, with a key derived from the site’s own authentication salt (wp_salt('auth')), using libsodium’s secretbox where available, otherwise AES-256-GCM via OpenSSL. Rotating the salts makes the stored key unreadable and it must be re-entered.
The key is stored base64-encoded and unencrypted, and the status page says so (secrets scheme: plain). On such a host, define NORG_SITE_KEY in wp-config.php instead.
Outbound calls and budgets
All calls use the WordPress HTTP API with TLS verification on and redirects off, and identify themselves as norg-edge-router-wp/{version}. The control headers on every call are X-Norg-Site-Id, X-Norg-Site-Key and X-Norg-Edge-Version. {api} is content-craft-api.norg.ai and {content} is edge-content.norg.ai unless overridden.
| Call | Endpoint | When | Budget | Sends |
|---|---|---|---|---|
| Policy feed | GET {api}/api/v1/edge/bot-patterns | When the cached feed is older than its cache_ttl (3,600 s default); refreshed on the next request, stale copy served meanwhile; kept up to 24 h while NORG is unreachable. A 401/403 switches serving off at once. | 5 s | Control headers, If-None-Match |
| Heartbeat | POST {api}/api/v1/edge/heartbeat | WP-Cron every 30 min; on traffic at most every 900 s; on connect and from the admin page. | 3 s | Plugin version, trigger, environment, platform, dropped-event count, runtime block (WordPress and PHP version, SAPI, server, cache plugins, host, bypass result, secrets scheme, multisite). |
| Mirror fetch | GET {content}/{site_id}{path}/index.html | Per verified agent request. | 4 s, 8 MB cap | Control headers, x-norg-edge: 1, X-Norg-Visit (base64url JSON, 4 KB max: agent user agent, classification, outcome, HTTP protocol, country when behind Cloudflare). |
| Render request | POST {api}/api/v1/edge/render-requests | Only on a definite 404 from the content service; de-duplicated per path for 600 s. | 3 s | path, url, reason: bot_miss |
| Update manifest | GET {api}/api/v1/edge/wordpress/update-manifest | Cached 6 h; forced when a heartbeat answers update_available. | 5 s (package download 120 s) | Control headers |
| MCP relay | POST {api}/public-mcp/ | When an MCP request addresses the site. | 10 s | Allow-listed headers, control headers, X-Norg-Edge-Page |
| Visit events (record-only) | POST {api}/api/v1/edge/events | After the response, in record-only mode. | 3 s | host, path, user agent, classification, status, verified flag, country, protocol |
| Citation page-views (record-only) | POST {api}/api/v1/edge/page-views | A person arriving from an AI answer or a search result, in record-only mode, while citation_tracking_enabled is on in the feed. | 3 s | Pseudonymous visitor id (truncated SHA-256 of address, user agent, Accept-Language and UTC date), user agent, referrer, utm_source/medium/campaign, country, device type, citation fields |
Page content from WordPress. Cookies or authorisation headers. Anything at all for an ordinary human visitor while the plugin is in serving mode. The one flow that describes people, citation page-views, runs only in record-only mode and is described in the table above exactly as it is built.
Admin, REST, CLI and health surfaces
Every surface that reads or changes the plugin’s state, and who it is open to.
- Settings screen. Settings → NORG Edge Router: Site ID and Site Key (masked; leave empty to keep), environment, visitor-address source, the switches above, status rows, the cache-plugin table, host advice, and the read-only list of agent user agents for your host.
- REST. GET norg-edge/v1/status and POST norg-edge/v1/connect, both requiring manage_options. NORG’s dashboard uses them with a WordPress Application Password you issue and can revoke. Connect never returns the key and answers 409 when the values are pinned in wp-config.php.
- WP-CLI. wp norg-edge configure, status, check, heartbeat and apply-cache-bypass, running the same code paths as the screen.
- Site Health. Three tests under Tools → Site Health: connection, AI agents reach the plugin, background tasks.
- Keyed health probe. A request carrying x-norg-edge-check equal to the Site Key (compared in constant time) receives a JSON status; off with NORG_EDGE_ORIGIN_PROBE.
Run checks
Run checks makes four loopback requests from the server to the site’s own home page, each with a 10 s budget. A loopback that cannot connect reports unverifiable rather than guessing.
| Run checks row | What it proves |
|---|---|
| Browser untouched | A browser user agent to the home page must come back without any X-Norg-Edge header. |
| AI agents reach plugin | A GPTBot user agent carrying the health-check key must reach PHP: reached, or cache_interference when a cache answered instead. |
| Search engine untouched | Googlebot with the simulate header must come back untouched: rule two, proven on your own site. |
| Agent diverted | GPTBot with the simulate header should return mirror, or stripped if no mirror exists yet. |
Updates
Updates come from NORG, not wordpress.org. The plugin hooks WordPress’s own update check for its Update URI on content-craft-api.norg.ai.
The manifest names a version, a package URL (a GitHub release asset) and its SHA-256. The plugin downloads the package itself and refuses it unless the file’s hash matches. A valid Site ID and key select the general or canary channel; the manifest itself carries no secret. Automatic installation follows NORG_EDGE_AUTO_UPDATE; with it off, the update appears on the Plugins screen for an administrator to approve. The status page reports when DISALLOW_FILE_MODS or AUTOMATIC_UPDATER_DISABLED prevents updates. Minimum WordPress 6.0 and PHP 7.4; no other runtime libraries.
Deactivation and uninstall
Deactivation stops everything; uninstall also removes every trace.
Deactivate removes the cron events, the entries it added to cache plugins, the .htaccess block if one was written, and its lock. Uninstall additionally deletes its options (settings included), the update-manifest transient and every _transient_norg_edge_* row. On request NORG deletes the site’s mirrors and record.
Write to solutions@norg.ai with your hostname and the section this is about. We will send the plugin source for the version you are being asked to install.
