Automating Sitemap Regeneration on CMS Publish

Wire your CMS publish event to a secured endpoint that regenerates the sitemap, stamps an accurate lastmod, and purges the cached copy so crawlers always fetch a current file.

When to Use This Approach

Automate sitemap regeneration when your publishing cadence outpaces your build cadence:

  • Editors publish or update content several times a day and a nightly or manual sitemap build leaves new URLs undiscovered for hours.
  • Your sitemap’s lastmod values are stale or all identical, so crawlers can no longer tell which pages actually changed.
  • You rely on manual sitemap regeneration and occasionally forget it after a publish, leaving the served file out of sync with live content and its canonical URLs.

Publish-triggered sitemap regeneration flow A CMS publish event calls a secured endpoint, which revalidates the sitemap route, purges the cached copy at the CDN, and pings search engines with the updated file. CMS Publish Secured Endpoint Revalidate Sitemap Purge CDN Cache Ping Engines verify secret lastmod=updatedAt purge /sitemap.xml

Implementation Steps

Step 1: Expose a Secured Publish Endpoint

Create an endpoint that verifies a shared secret or HMAC signature before doing any work. An unauthenticated regeneration hook is a denial-of-service lever, so reject anything that fails verification with a 401 before touching the sitemap.

// api/sitemap-hook.js — Next.js Route Handler / serverless function
import crypto from 'node:crypto';

export async function POST(req) {
  const signature = req.headers.get('x-cms-signature') || '';
  const body = await req.text();
  const expected = crypto
    .createHmac('sha256', process.env.SITEMAP_HOOK_SECRET)
    .update(body)
    .digest('hex');

  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!ok) return new Response('invalid signature', { status: 401 });

  const { slug } = JSON.parse(body);
  await regenerate(slug); // steps 2-4 below
  return Response.json({ ok: true });
}

Validation:

# A request with no/invalid signature must be rejected
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
  https://staging.yourdomain.com/api/sitemap-hook -d '{"slug":"/x/"}'
# Expected: 401

Step 2: Regenerate or Revalidate the Sitemap Route

Rather than rebuilding the whole site, revalidate just the sitemap route so the next request rebuilds it from fresh CMS data. On-demand revalidation keeps the turnaround to seconds instead of a full deploy.

// inside regenerate(slug)
import { revalidatePath } from 'next/cache';

async function regenerate(slug) {
  // Invalidate the cached sitemap render and the changed page itself
  revalidatePath('/sitemap.xml');
  if (slug) revalidatePath(slug);
  await purgeAndPing(); // steps 3-4
}

Validation:

# Fetch twice; the second response should reflect the new entry
curl -s https://staging.yourdomain.com/sitemap.xml | grep -c "<url>"
# The count should increase by one after publishing a new entry.

Step 3: Set lastmod From the CMS updatedAt Field

Drive each URL’s lastmod from the entry’s own updatedAt timestamp, not the sitemap build time. A per-entry timestamp is the signal that tells crawlers which specific pages to recrawl.

// app/sitemap.js — sitemap generator reads CMS timestamps
export default async function sitemap() {
  const entries = await cms.getAllEntries(); // { path, updatedAt }
  return entries.map((e) => ({
    url: `https://yourdomain.com${e.path}`,
    lastModified: new Date(e.updatedAt), // NOT Date.now()
    changeFrequency: 'daily',
  }));
}

Validation:

# The changed URL's lastmod must match its CMS updatedAt, and peers must NOT all share one timestamp
curl -s https://staging.yourdomain.com/sitemap.xml \
  | grep -A1 "/blog/new-post/" | grep -oP '(?<=<lastmod>)[^<]+'

Step 4: Purge the CDN Copy of the Sitemap

The sitemap is a cacheable object, so a regenerated origin file is invisible until you invalidate the edge copy. Purge the specific sitemap path on every publish.

# Cloudflare purge of the sitemap object only
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache" \
  -H "Authorization: Bearer ${CF_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"files":["https://yourdomain.com/sitemap.xml"]}'

Validation:

# Confirm the edge now serves a MISS/refreshed copy
curl -sI https://yourdomain.com/sitemap.xml | grep -i "cf-cache-status\|age"
# Age should reset toward 0 immediately after purge.

Step 5: Resubmit and Validate

Debounce a resubmission so bursts of publishes do not fire one request each, then confirm the served file is current. Keep the canonical URLs in the sitemap identical to the ones your pages self-reference.

# Confirm the changed URL is present with a fresh lastmod, then resubmit in Search Console
curl -s https://yourdomain.com/sitemap.xml \
  | grep -c "<loc>https://yourdomain.com/blog/new-post/</loc>"
# Expected: 1  — then use GSC "Sitemaps" to resubmit (debounced, not per-publish)

SEO Impact Summary

Signal What improves What breaks if misconfigured
Discovery latency New URLs appear in the sitemap seconds after publish Manual builds leave fresh content undiscoverable for hours or days
lastmod trust Per-entry updatedAt gives crawlers an actionable recrawl signal Build-time lastmod on every URL trains crawlers to ignore the field
Cache correctness Purging serves the regenerated file at the edge immediately A cached stale sitemap masks every regeneration until TTL expiry
Crawl efficiency Accurate change signals focus recrawl on pages that moved Noisy or absent lastmod spreads crawl budget across unchanged URLs

Measurable signals to watch:

  • GSC Sitemaps report: “discovered URLs” should track published-entry count with no lag after a publish.
  • Time from publish to URL appearing in the served sitemap should drop from hours to under a minute.
  • Crawl-stats recrawl of changed URLs should rise while recrawl of unchanged URLs falls, reflecting trusted lastmod.

Edge Cases and Gotchas

Unpublish and delete events A publish webhook covers additions and edits, but deletions must also fire the hook so the removed URL leaves the sitemap. Handle the CMS unpublish/delete event the same way and confirm the <loc> disappears; a lingering entry sends crawlers to a soft 404.

Burst publishes and webhook storms A bulk import can fire hundreds of publish events in seconds. Debounce regeneration with a short trailing window (for example 10 seconds) so many events collapse into one rebuild and one CDN purge, preventing rate-limit errors on the purge API.

Draft and preview leakage The webhook may also fire for draft saves in some CMS platforms. Filter on publication status so drafts never enter the sitemap, and make sure the generator reads the published field only — the same discipline that keeps preview URLs out of your slug pipeline.

Very large sitemaps hit the size ceiling Regeneration works the same up to the protocol limits, but a single file caps at 50,000 URLs or 50 MB uncompressed. Once you approach that, move to a sitemap index with sharded children and regenerate only the shard that contains the changed URL.

Frequently Asked Questions

Should lastmod reflect the CMS update time? Yes. Read lastmod from the entry’s updatedAt field in the CMS, never from the sitemap build time. If every URL carries the build timestamp, crawlers quickly learn the field is meaningless for your site and stop using it to prioritize recrawl — which defeats the point of regenerating on publish at all.

How do I trigger a sitemap rebuild on publish? Register a publish webhook in your CMS that posts to a secured endpoint on your frontend. That endpoint verifies the signature, revalidates the sitemap route so it rebuilds from current data, and purges the CDN copy. For dynamic sitemaps in a composable CMS the same webhook can also warm the newly published page.

Do I need to ping search engines after each change? No. A ping is only a nudge, and search engines recrawl sitemaps on their own cadence regardless. Firing one ping per publish adds noise and can trip rate limits, so debounce them into an occasional batch. Spend the effort instead on an accurate lastmod and a reliably purged CDN cache.

What if the regeneration endpoint fails mid-publish? Make the hook idempotent and safe to retry: verify, revalidate, purge, and return a status the CMS can act on. Configure the CMS to retry on non-2xx responses, and add a scheduled full regeneration as a backstop so a missed webhook self-heals within the next cycle.


Part of: XML Sitemap Generation for Headless

Related