Documentation · Edge Cache for ExpressionEngine

Surgical tag-based purges for any tag-aware edge.

The complete reference for the add-on — how tag-based purging works, install, template integration, every backend, MSM topology, and troubleshooting. Skim the TOC to jump straight to what you need.

Add-on version: v2.4.27 Tested on EE 7.x PHP 7.4+ MIT licensed

1.How tag-based caching works

If you've only used URL-based purges before — "when /news/foo updates, purge /news/foo" — tag-based is the upgrade. The page advertises what it contains; the edge purges by content identity.

The chain in one sentence

Every page emits a list of tags describing what's on it (entry-123, category-9, channel-news, home). When an editor saves entry 123 in the CP, this add-on POSTs a purge for the tags entry-123 channel-news home. The edge cache evicts every page carrying any of those tags — single-entry view, channel listing, homepage — in one call.

What you store, what you purge

There is no "page → tag" KV. The edge stores cached_body + Surrogate-Key: <tags> together. When a purge arrives, the edge walks its cache and evicts every cached body whose tag set intersects the purge tag set.

That's why tag granularity matters: the more accurately each rendered page describes its content, the more precisely a purge can target.

Why entry IDs and not URLs?

  • Stability. Entry IDs never change. URL titles change on slug edits; URLs change when you reorganize taxonomies. ID-tagged cache survives all of that.
  • Cross-page coverage. The same entry appears on many URLs (single view, channel index, category archive, search). One entry-N tag intersects all of them.
  • Save-event compatibility. EE's after_channel_entry_save hook hands the add-on the entry id directly — no URL list to maintain.

2.Quick start

  1. Download the latest zip from github.com/calimonk/ee-edge-cache-tags/releases/latest.
  2. Extract to system/user/addons/edge_cache_tags/.
  3. Install via CP → Developer → Add-Ons → click Install on Edge Cache Tags.
  4. Configure: CP → Edge Cache → Setup, pick a backend, paste credentials, save.
  5. Verify: edit any channel entry; the Activity tab should show a purge row within a second.
TipHeaders emit immediately — even with the backend set to none, every front-end response carries Surrogate-Key. You can install the add-on and see headers in curl -I output right away. Pick a backend when you're ready for auto-purges too.

3.What gets emitted on every page

The add-on auto-tags from the URI on every front-end GET. Example for /news/some-article:

Surrogate-Key: path-news home all
Cache-Tag:     path-news,home,all     // only when backend = cloudflare
TagWhen emitted
path-<first-segment>Any URL with a path. /news/foopath-news.
homeHomepage / front controller root.
allEvery page (lets an admin nuke everything via one manual purge).
site-<id>-*MSM only: every tag prefixed with the site id, plus an unprefixed all.
entry-N / channel-X / category-NOnly when templates declare them — see next section.
NoteThere's no auto-emitted "rendered by template X" tag. EE's template_post_parse fires multiple times per page (URL template → embeds → layout), so any auto-captured value tracked the last-parsed template (usually the layout) — too broad to be useful. Push template-scoped tags explicitly with {exp:edge_cache_tags:key name="tmpl-news-index"}.

4.Template integration

For each page that displays entry data, declare which entries are on it. Use the plugin tag {exp:edge_cache_tags:key name="…"} — it outputs nothing, just registers tags. They emit in the Surrogate-Key header on render.

Pattern 1 — single entry view

For /news/some-article showing one entry's full content:

{!-- templates/news/_view.html --}
{exp:channel:entries channel="news" limit="1"}
  {exp:edge_cache_tags:key name="entry-{entry_id} channel-news"}

  {!-- Optional: only if you use EE categories --}
  {categories}{exp:edge_cache_tags:key name="category-{category_id}"}{/categories}

  <article>
    <h1>{title}</h1>
    {body}
  </article>
{/exp:channel:entries}

Now editing this entry, changing its categories, or deleting it evicts this page.

Pattern 2 — listing / index page

For /news/ showing the latest 20 entries:

{!-- templates/news/index.html --}
{exp:channel:entries channel="news" limit="20"}
  {exp:edge_cache_tags:key name="entry-{entry_id}"}
  <a href="{url_title_path='news'}">{title}</a>
{/exp:channel:entries}
{exp:edge_cache_tags:key name="channel-news"}

Listing pages tag each entry they display, plus the channel. Saving any one of those 20 entries purges this listing. Adding a 21st entry also purges it via the channel-news tag.

Pattern 3 — paginated listings

For /news/P20, /news/P40, … every paginated page runs the same template, so they all emit channel-news. Each page additionally tags the entries currently visible on it.

  • Edit entry-50 → fires entry-50 channel-news → page 3 (which had entry-50) evicts via entry-50, the rest evict via channel-news. All pagination refreshes together.
  • Add a new entry → fires channel-news → all pagination pages evict (a new entry shifts the order).
  • Delete entry-25 → same: channel-news fires, all pages refresh.

channel-<name> is the load-bearing tag for paginated listings. Make sure your listing template emits it.

Pattern 4 — your own taxonomy

Tags are arbitrary strings. The built-in conventions exist because EE hands the add-on channel and category data through hooks — outside that, invent any taxonomy that makes sense.

{exp:channel:entries channel="articles" limit="1"}
  {exp:edge_cache_tags:key name="entry-{entry_id} channel-articles"}

  {!-- Custom: tag with each related author and topic --}
  {your_authors_field}{exp:edge_cache_tags:key name="author-{author_slug}"}{/your_authors_field}
  {your_topics_field}{exp:edge_cache_tags:key name="topic-{topic_slug}"}{/your_topics_field}
{/exp:channel:entries}

Now the page is tagged entry-N channel-articles author-jane topic-pricing. Purging author-jane evicts every page bylined to her, in one call.

Triggering custom-tag purges: the add-on auto-purges only the standard tags (entry-N, channel-X, path-X, category-Y, home). For custom taxonomies, use the Quick actions panel on the Setup tab, or call Edge_cache_tags_ext::manual_purge_tags(['author-jane']) from a custom extension.

5.What gets purged on save

When an editor hits Save on a channel entry (or deletes one), the add-on dispatches a single POST per backend listing these tags:

TagWhat it evicts
entry-<id>Every page that featured this entry
channel-<name>Channel listing pages declared in templates
path-<name>Every URL under /<name>/…
category-<cat_id> ×NOne per category the entry belongs to — category archives
homeThe homepage (entries often appear there)
site-<id>-*MSM only — all the above prefixed for isolation
Notable absenceall is not auto-purged on save. Every page emits it so an admin can nuke the whole cache via Quick actions — but firing it on every save would evict every cached page on every publish, defeating the surgical-purge premise.

Multiple saves in the same CP request coalesce into one POST per backend. Bounded 5-second timeout — a slow edge never blocks a CP save for long.

6.Configuration

Three ways to configure, in order of precedence (highest first):

  1. $assign_to_config in your front controller (per-MSM-site overrides — front-end only)
  2. config.php — shared across all MSM sites; pin values code-side
  3. CP form — Edge Cache → Setup; stored in exp_edge_cache_tags_settings per site_id

Values from a higher-priority source pin the field in the CP form with a 🔒 lock indicator.

CP form (recommended for non-developers)

Switch the EE site picker first if MSM, then CP → Edge Cache → Setup, pick backend, fill credentials, save. Each MSM site has its own row.

config.php (developers / config-as-code)

// Pick ONE of the blocks below.

// Cloudflare Enterprise
$config['edge_cache_tags_backend']      = 'cloudflare';
$config['edge_cache_tags_cf_zone_id']   = 'abc123...';
$config['edge_cache_tags_cf_api_token'] = '...';

// Fastly
$config['edge_cache_tags_backend']         = 'fastly';
$config['edge_cache_tags_fastly_service']  = 'SU1Z0...';
$config['edge_cache_tags_fastly_api_key']  = '...';

// Nivoli
$config['edge_cache_tags_backend']         = 'nivoli';
$config['edge_cache_tags_nivoli_endpoint'] = 'https://console.nivoli.com/cache/<token>';

// Generic webhook
$config['edge_cache_tags_backend']        = 'webhook';
$config['edge_cache_tags_webhook_url']    = 'https://cache.example.com/purge';
$config['edge_cache_tags_webhook_secret'] = '...';

$assign_to_config per front controller

If your MSM setup uses separate index.php per site, put per-site overrides there:

// index.site-a.php
$assign_to_config['template_group'] = 'home';
$assign_to_config['template']       = 'index';

$assign_to_config['edge_cache_tags_backend']      = 'cloudflare';
$assign_to_config['edge_cache_tags_cf_zone_id']   = '...';
$assign_to_config['edge_cache_tags_cf_api_token'] = '...';
Gotcha$assign_to_config in index.php is front-end only. The Control Panel runs through admin.php. Values set in index.php ARE seen on front-end emits + auto-purges, but the CP can't read them — you'll see "unknown backend" in Diagnostics. Either put them in config.php (shared) or mirror them into admin.php too, gated by hostname.

7.Backends

Pick once; switch any time. Headers always emit. The backend choice is just where purge calls go.

BackendReadsPurge endpoint
fastly Surrogate-Key POST /service/<id>/purge with Surrogate-Key: header. Standard plans OK. Token needs purge permission.
cloudflare Cache-Tag POST /zones/<id>/purge_cache with {tags:[…]}. Requires CF Enterprise — Cache-Tag purging is gated. Token scoped to Zone → Cache Purge → Purge.
nivoli Surrogate-Key POST <dashboard>/purge-tag with {tags:[…]}. Managed service with this add-on pre-wired.
webhook (your edge) POST JSON {tags, site_id, source} to your URL. Optional Authorization: Bearer <secret>. Great for Varnish/xkey or a custom proxy.
none Surrogate-Key Headers emit; no purge dispatch. Useful for VCL-managed Varnish or testing.

8.Multi-Site Manager (MSM)

The add-on is MSM-aware. Behavior splits cleanly between default site and secondary sites.

Default site (site_id = 1)

Keys are emitted and purged unprefixedhome, entry-123, channel-news, etc. Works the same as a single-site install.

Secondary sites (site_id > 1)

All keys are prefixed with site-<id>- for cross-site isolation:

  • A page on site 2 emits site-2-home site-2-entry-123 … plus an unprefixed all (so an admin can still do a network-wide nuke by purging all)
  • A save on site 2 purges only site-2-* tags — it does not touch the unprefixed all. Saving an article on site 2 does not blow away site 1's cache.

One backend, many sites

One EE install with N sites uses one purge backend (one Fastly service / one Cloudflare zone / one Nivoli token). The site-<id>- prefixing keeps tag namespaces separate.

Nivoli on MSMBy default each MSM hostname is its own Nivoli tenant with its own token. To share a single endpoint across MSM sites, ask Nivoli support to link your tokens — one dashboard URL then accepts purges for all linked hostnames, and the site-<id>- prefixing routes them to the right hostname automatically. The Setup tab's host-scope banner confirms the current site is in the token's allowed list.

9.Trace mode

If headers don't appear on a page where you expect them:

curl -I "https://yoursite.com/some-page?ect_trace=1"

Look for X-Edge-Cache-Tags-Trace in the response. It records every decision the emit code path made — whether the response was cacheable, which method, how many keys were produced, whether each header was emitted. Diagnoses missing headers without enabling verbose logging.

If trace shows nothingYour edge is serving a cached HIT from before the add-on was installed. Bust the cache with a one-shot query string:
curl -I "https://yoursite.com/page?nocache=$(date +%s)"

10.Common questions

Do I need a backend selected before headers emit?

No. Headers emit on every front-end GET that goes through the EE template engine, regardless of which backend is selected — including none. The backend choice only controls where purges get dispatched on save.

How can headers come out before the body if the template tag is inside the HTML?

Output buffering. The template engine runs all tags first (including {exp:edge_cache_tags:key}, which stashes keys in a session cache and outputs nothing), then template_post_parse fires, then the add-on reads the stashed keys and calls ee()->output->set_header(...) to register the headers. EE finalizes output: HTTP headers go out first (yours included), then the body.

So template tag order doesn't matter — put the plugin tag anywhere inside <html>…</html>.

I curled my site and didn't see the headers. What gives?

Almost always: your edge cache is still serving the version it cached before the add-on was installed. Look for cf-cache-status: HIT — that confirms the answer didn't come from EE just now. Use the cache-bust query string above.

Why don't my archive / listing pages evict when I save an entry?

A save dispatches home + entry-N + channel-X + path-X + category-N…, so an archive page only evicts if it emitted one of those tags when it rendered. Make sure your listing and category templates push {exp:edge_cache_tags:key name="channel-<name>"} (and a category-<id> key on category archives) — channel-<name> is the load-bearing tag for listings. See Template integration.

Can I disable the add-on without CP access?

Yes — set $config['edge_cache_tags_disable'] = true; in your front controller's assign_to_config. The hook handlers will short-circuit without touching the CP.

11.Recommended companions

Add-on Manager (free)

If you don't want to FTP zips into system/user/addons/, Add-on Manager by Javid Fazaeli is a useful free tool — drag a zip into the CP, it installs the add-on for you. No FTP / SCP required.

Note: it doesn't poll for new versions of installed add-ons. Edge Cache Tags ships its own GitHub-releases poller for that — update notifications appear in the CP either way.

Related