Purging Edge Caches on CMS Publish Without SEO Gaps

The publish webhook has to invalidate everything the change affects and nothing else — a full purge is easy to implement and turns every editorial update into a thundering herd against the origin.

When to Use This Approach

Implement targeted purging when:

  • Publishes currently trigger a full cache purge, or nothing at all.
  • Editors report that changes appear on the entry page but not in listings.
  • Origin load spikes correlate with editorial activity rather than with traffic.

The Affected Set Is Larger Than One URL

Publishing an article changes the article page, the listings that include it, the author page, the tag pages, and the sitemap. Purging only the first is why editors report that “the change did not go live”.

The pages one publish actually affects A single published entry connected to five affected surfaces — its own page, category listings, the author page, pages that embed it, and the sitemap. entry published one webhook its own page category and tag listings author page pages embedding it sitemap entry and lastmod always often missed often missed usually missed must be atomic

Implementation Steps

Step 1: Tag responses with the entities they render

The dependency map is built at render time, when the page knows exactly what it read.

// lib/render-tracking.js — collect entity ids during a render
export function withTracking(render) {
  return async (req) => {
    const touched = new Set();
    const cms = trackingClient(touched);           // records every id it returns
    const res = await render(req, cms);
    res.headers.set('Surrogate-Key', [...touched].join(' '));
    return res;
  };
}
Surrogate-Key: entry:8f21 author:113 category:running asset:9d02 template:article

SEO impact: Every cached response carries an accurate list of what would make it stale, so invalidation can be precise instead of wholesale.

Validation: curl -sI <listing url> | grep -i surrogate-key should list every entry currently shown on that listing.

Step 2: Compute the affected set from the publish event

// app/api/webhooks/publish/route.js
export async function POST(request) {
  const event = await request.json();
  const entry = await cms.get(event.entryId);

  const keys = new Set([`entry:${entry.id}`]);
  for (const ref of collectReferences(entry)) keys.add(`entry:${ref.id}`);
  if (entry.author) keys.add(`author:${entry.author.id}`);
  for (const c of entry.categories ?? []) keys.add(`category:${c.slug}`);

  await purgeByKeys([...keys]);
  await regenerateSitemap(entry);
  await warm(topAffectedUrls(entry));

  return Response.json({ purged: keys.size });
}

Validation: Publish an entry and confirm the response reports more than one key. A count of one means references are not being collected.

Step 3: Purge by tag, not wholesale

// lib/purge.js
export async function purgeByKeys(keys) {
  const res = await fetch(`https://api.cdn.example/zones/${ZONE}/purge`, {
    method: 'POST',
    headers: { authorization: `Bearer ${CDN_TOKEN}`, 'content-type': 'application/json' },
    body: JSON.stringify({ tags: keys }),          // never { purge_everything: true }
  });
  if (!res.ok) throw new Error(`purge failed: ${res.status}`);
}

SEO impact: Unrelated pages stay warm, so a publish during a crawl burst does not force hundreds of origin renders and the slow responses that follow.

Validation: Measure origin request rate before and after a publish. A targeted purge should be nearly invisible in that metric.

Step 4: Warm the important pages immediately

// lib/warm.js — re-request the highest-value affected URLs
export async function warm(urls, concurrency = 4) {
  const queue = [...urls];
  const workers = Array.from({ length: concurrency }, async () => {
    while (queue.length) {
      const url = queue.shift();
      await fetch(url, { headers: { 'user-agent': 'cache-warmer' } }).catch(() => {});
    }
  });
  await Promise.all(workers);
}

SEO impact: The window in which a crawler could hit a cold cache is measured in seconds rather than until the next organic visitor.

Validation: Immediately after a publish, request the entry URL and confirm a cache hit rather than a miss.

Step 5: Regenerate the sitemap in the same operation

// The purge and the sitemap must not diverge
export async function regenerateSitemap(entry) {
  await recordPublish(entry);                       // updates the stored lastmod
  await rebuildSitemapShard(entry.type);
  await purgeByKeys(['sitemap']);
}

SEO impact: The sitemap’s lastmod and the served content change together, so the crawl signal is never advertising a change the cache has not yet made visible. The timestamp rules are in adding lastmod accurately from CMS timestamps.

Validation: After a publish, the sitemap lastmod and the page’s rendered content should both reflect the change on the same request.

Purge Strategies Compared

The three approaches differ by an order of magnitude in both precision and blast radius.

Full purge, URL purge, and tag purge compared Three strategies rated on how precisely they invalidate, how much origin load they cause, and what they cost to implement. strategy precision origin load effort purge everything none one line purge by URL the entry only low purge by tag exact affected set render tracking Purging by URL is precise about the wrong thing — it misses every listing Tag purging costs one render-time hook and removes both failure modes

SEO Impact Summary

The moments after a purge are when the cache is coldest, which is exactly when a crawler arriving would get the slowest response — hence the warming step.

The cold window after a purge A timeline showing a cold cache window immediately after a purge, shortened to seconds by warming the most important affected URLs. no warming cold until organic traffic arrives with warming warm within seconds purge issued The cold window is short but it is the window a crawl burst can land in
Signal What improves What breaks if misconfigured
Content freshness Every surface showing an entry updates together on publish URL-only purging leaves listings showing stale titles and excerpts
Origin stability A publish causes a handful of renders instead of a full repopulation Full purges during crawl bursts produce 5xx responses and crawl-rate throttling
Crawl consistency Sitemap lastmod and served content change atomically A sitemap advertising a change the cache has not made visible wastes the crawl
Recovery speed Warmed pages mean the first post-publish crawler hits a warm cache An unwarmed purge leaves a cold window of unpredictable length

Measurable signals to watch:

  • Origin request rate around publish events, which should be nearly flat.
  • Cache hit ratio in the minutes after a publish.
  • Time from publish to updated content on listings, not just on the entry page.

Edge Cases and Gotchas

Tag limits are real CDNs cap the number of surrogate keys per response and per purge request. A listing rendering 100 entries can exceed the per-response limit, so tag by collection where individual entries are too numerous — category:running rather than 100 entry keys.

A purge is not instantaneous across regions Propagation takes seconds to tens of seconds. Warming immediately after issuing a purge can re-cache the old response if the purge has not reached that region. Add a short delay, or warm through a cache-bypass header and let the next request populate.

Unpublishing needs the status code to change too Purging an unpublished entry’s cache is not enough if the route still renders it from a stale data cache. Remove the entry from the sitemap and confirm the URL now returns the intended status — the options are covered in noindex vs robots Disallow for headless routes.

Webhook delivery is not guaranteed Every CMS webhook occasionally fails to deliver. The shared cache lifetime is your backstop, which is another reason to set it deliberately rather than to infinity — the values are in setting Cache-Control headers for crawlable pages.

Frequently Asked Questions

Why is a full purge harmful? It converts every subsequent request into an origin render at once. During a crawl burst that produces slow responses and errors, and Google reduces crawl rate in response to both. On a busy site, full purging on every publish is a self-inflicted incident several times a day.

How do I know which listings include an entry? Record it at render time. A page knows exactly which entities it read, so tagging the response with those identifiers builds the dependency map as a side effect of serving traffic — and it stays correct as templates change.

Should unpublishing purge differently from publishing? The purge is the same; the bookkeeping is not. An unpublish must also remove the URL from the sitemap and start returning the correct status, or the sitemap advertises a page that no longer exists.

Is stale-while-revalidate an alternative to purging? No, a complement. It removes the latency cost of expiry but not the staleness itself, so a change still waits out the window. Purging makes the change immediate; the stale window covers whatever the purge missed.


Part of: Edge Caching Behavior for SEO

Related