Edgenote
Cache-Control header helper for Cloudflare and edge CDNs. Surgically overrides WordPress 6.8+'s aggressive nocache_headers on anonymous public requests. MIT.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/rennerdo30/wp-edgenote/archive/refs/heads/main.zipWordPress 6.8+ kills your edge cache. Edgenote brings it back.
A surgical Cache-Control header helper for Cloudflare and other edge CDNs.
The problem
WordPress 6.8 changed wp_get_nocache_headers() so it now always emits:
Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private
— regardless of whether the request is anonymous or authenticated. Cloudflare (and any well-behaved shared cache) refuses to cache anything containing private or no-store. The result: every visitor hits the origin every time, TTFB craters, and the "edge cache" is decorative.
The header gets emitted from at least three different places in core (nocache_headers filter, wp_headers filter, and direct header() calls in send_headers), so a half-fix that only hooks one of them gets quietly clobbered by the other two.
The fix
Edgenote hooks all three insertion points and runs at low priority so it always wins:
nocache_headersfilter (priority 99) — replacesCache-Control, dropsExpires/Pragma, removesCDN-Cache-Control.wp_headersfilter (priority 99) — replacesCache-Control, setsVary: Accept-Encoding, Cookie, removesCDN-Cache-Control.send_headersaction (priority 999) — explicitheader(..., true)call as the final word, plusheader_remove('CDN-Cache-Control')for the rogue header WP 6.8 sometimes emits direct.
Only fires on requests that are actually safe to cache:
- Anonymous (no logged-in user, no
wordpress_logged_in_*/comment_*/wp-postpass_*cookies) GETorHEADonly- Not admin, AJAX, REST, search, feed, 404, or preview
- Path doesn't match the bypass list (
/wp-admin,/wp-login.php,/feed)
Emitted Cache-Control on a cacheable request:
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
Vary: Accept-Encoding, Cookie
max-age=0 means the browser never caches (so a logged-in admin who returns to a page after login still gets fresh content). s-maxage means the edge caches for 5 minutes by default. stale-while-revalidate lets the edge serve stale while refreshing.
Quick start
- Drop the plugin into
wp-content/plugins/edgenote/and activate (or zip-install). - Defaults are sane: 5-minute edge TTL, 1-day stale-while-revalidate.
- Settings → Edgenote to tune. Click Test headers to verify what visitors see.
Configuration
Settings → Edgenote (option key edgenote_settings):
| Field | Default | Purpose |
|---|---|---|
Edge cache TTL (s-maxage) |
300 |
Seconds shared caches may serve a cached copy |
| Stale-while-revalidate | 86400 |
Seconds shared caches may serve stale during background refresh |
| Bypass cookies | wordpress_logged_in_, comment_, wp-postpass_ |
Cookie name prefixes that mark a request as authenticated |
| Bypass paths | /wp-admin, /wp-login.php, /feed |
URL fragments that always skip the override |
Cookie and path lists are one prefix per line; substring match against REQUEST_URI for paths and against the cookie name for cookies.
Cloudflare setup
Two ways to make Cloudflare honor the Cache-Control we emit:
- Caching → Configuration → Respect Existing Headers (recommended). Cloudflare obeys whatever
s-maxageEdgenote sets. - Page Rule with Edge Cache TTL set explicitly. Bypasses Cache-Control entirely; Edgenote still helps because Cloudflare's "cache by default" rules require Cache-Control to be public.
After setup, hit a public URL with curl -I https://yoursite.com/ and look for cf-cache-status: HIT after the first request.
Architecture
wp boot
│
▼
┌──────────────────────────┐
│ CacheableRequest │ anonymous? GET/HEAD?
│ - method check │ not admin/REST/AJAX/preview/search/feed?
│ - cookie bypass list │ no bypass-cookie? no bypass-path?
│ - path bypass list │
└────────────┬─────────────┘
│ yes
▼
┌──────────────────────────────────────────────┐
│ HeaderOverride │
│ ├ nocache_headers filter (priority 99) │
│ ├ wp_headers filter (priority 99) │
│ └ send_headers action (priority 999) │
│ │
│ sets: │
│ Cache-Control: public, max-age=0, │
│ s-maxage=N, stale-while-revalidate=M │
│ Vary: Accept-Encoding, Cookie │
│ strips: │
│ Expires, Pragma, CDN-Cache-Control │
└──────────────────────────────────────────────┘
Known limitations
- Anonymous-only. Logged-in users always bypass the override; per-user edge caching is out of scope.
- No per-route TTL. A single
s-maxagecovers every cacheable request. Use a Cloudflare Page Rule or aCache Ruleif you need per-path control. - No purge integration. Edgenote sets headers; it does not call Cloudflare's purge API on
save_post. Pair with a purge plugin (e.g.cloudflare,wp-rocket) if you need invalidation on content updates. - Substring path match. A bypass entry
/feedalso bypasses/category/feedback/. Tune the bypass list to match the literal URL prefix.
Author
License
MIT.