· Refik Tefik

XM Cloud content published but not updating: how to find the layer that's stale

Published but still stale? Debugging XM Cloud content from Edge to the browser

An editor publishes a new headline in Sitecore XM Cloud. Preview looks correct, the publishing job finishes, and the public website still shows yesterday's copy. Publishing again may appear to help, but it does not explain which layer served the old value.

The useful question is: where does the new value stop appearing? Check the published item, the page's layout data, the rendered response, and finally the browser. Each comparison narrows the problem before you change any cache settings.

This practical guide takes its starting question from Amro Mustafa's “Why can published XM Cloud content still appear stale?” and adds independent diagnostic and revalidation examples.

Publishing and rendering are separate steps

A typical request travels through this chain:

ps
XM Cloud authoring -> publishing -> Experience Edge -> rendering application -> hosting CDN -> browser

Preview can show draft content while the Delivery API serves published content. Check the live environment and credentials before comparing results. Sitecore documents both direct Delivery API access and access through a live context ID in its API authorization guide.

An Edge cache clear also does not regenerate HTML already stored by your rendering host. Sitecore's publishing process documentation describes Edge finalization, cache clearing, and downstream webhooks as distinct operations. Follow the actual job status and webhook delivery logs; a fixed sleep is a poor substitute for confirming the expected content is available.

1. Ask Experience Edge for the exact field

Record the affected item ID, language, field name, and expected value. If the text belongs to a component datasource, use that datasource's ID. A route item's title may have nothing to do with the text the component displays.

Run this query against your Delivery schema in the GraphQL IDE. Replace the variables with your item details:

gql
query CheckPublishedField($id: String!, $language: String!, $field: String!) {
  item(path: $id, language: $language) {
    id
    field(name: $field) {
      value
    }
  }
}

For a repeatable command-line check, save the following as check-edge.mjs. It uses Node.js 20+ and the direct Delivery endpoint with a Delivery API token. Projects using the context-ID endpoint should adapt the URL and authentication header together, following the authorization guide above.

js
// check-edge.mjs
const required = (name) => {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
};

async function main() {
  const expected = required('EXPECTED_VALUE');
  const response = await fetch('https://edge.sitecorecloud.io/api/graphql/v1', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      sc_apikey: required('SITECORE_EDGE_TOKEN'),
    },
    signal: AbortSignal.timeout(15000),
    body: JSON.stringify({
      query: `query CheckPublishedField(
        $id: String!, $language: String!, $field: String!
      ) {
        item(path: $id, language: $language) {
          id
          field(name: $field) { value }
        }
      }`,
      variables: {
        id: required('SITECORE_ITEM_ID'),
        language: required('SITECORE_LANGUAGE'),
        field: required('SITECORE_FIELD'),
      },
    }),
  });
  if (!response.ok) throw new Error(`Delivery HTTP ${response.status}`);
  const payload = await response.json();
  if (payload.errors?.length) throw new Error('Delivery returned GraphQL errors');
  const item = payload.data?.item;
  if (!item?.field) throw new Error('Item or field missing; check ID, language and publication');
  const matchesExpected = item.field.value === expected;
  console.log(JSON.stringify({ itemId: item.id, matchesExpected }, null, 2));
  if (!matchesExpected) process.exitCode = 2;
}

main().catch((error) => {
  console.error(error.message);
  process.exitCode = 1;
});

Provide the token through your local secret environment or CI secret store. These PowerShell commands set only the diagnostic inputs:

ps
$env:SITECORE_ITEM_ID = '{YOUR-ITEM-GUID}'
$env:SITECORE_LANGUAGE = 'en'
$env:SITECORE_FIELD = 'Title'
$env:EXPECTED_VALUE = 'The newly published headline'
node ./check-edge.mjs

Exit code 0 means an exact match, 2 means a different field value, and 1 means a configuration or request failure. Rich-text values contain markup, so compare their raw value or select a plain-text field. The script does not print the token or field content. It checks what Edge serves; it does not bypass or purge Edge's own cache.

If Edge returns the old value, inspect the target environment, language version, workflow eligibility, publishing restrictions, and job errors. If the item is missing, investigate publication and identifiers before blaming the frontend.

2. Compare the data the page actually consumes

A matching field is useful evidence, but it is not proof that the page receives the same data. Replay the rendering application's actual layout or GraphQL request using the same site, route, language, and live credentials.

For example, a component can read a different datasource, a shared navigation item, or a search result. Publishing the page alone may not update every dependency. Sitecore documents differences between snapshot and runtime publishing, including integrated GraphQL behavior, in Publishing to Experience Edge. Check which publishing mode your environment uses before applying dependency advice.

Use the results to choose the next action:

  • The field is old: investigate publishing and the Delivery environment.

  • The field is current but layout data is old: inspect datasource selection, dependencies, and publishing mode.

  • Layout data is current but HTML is old: inspect application data caches, static output, regeneration logs, and the hosting CDN.

  • HTML is current but the screen is old: inspect browser storage, service workers, client data fetching, and hydration.

Also compare the same language and personalization state. Two different variants can legitimately show different copy.

3. Refresh the affected Next.js page

The next example targets a Next.js Pages Router application using ISR. It belongs in that Sitecore rendering application; the AHD website hosting this article uses Astro and Umbraco.

Next.js supports on-demand page regeneration with res.revalidate(). A timed ISR interval is request-driven and can serve an old response while regeneration runs; it is not a publication deadline. See the Pages Router ISR guide.

Save this handler as pages/api/revalidate.js in the rendering application. Replace the sample paths with the actual internal paths Next.js renders, including any locale or Sitecore rewrite prefixes. Configure a long random REVALIDATION_SECRET in the server environment.

js
// pages/api/revalidate.js
import { timingSafeEqual } from 'node:crypto';

// Replace with your application's real internal page paths.
const allowedPaths = new Set(['/en/news/platform-update', '/en/news']);

export default async function handler(req, res) {
  res.setHeader('Cache-Control', 'no-store');
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    return res.status(405).json({ error: 'Use POST' });
  }

  const secret = process.env.REVALIDATION_SECRET;
  if (!secret) return res.status(503).json({ error: 'Revalidation is not configured' });
  const header = req.headers.authorization;
  if (typeof header !== 'string') return res.status(401).json({ error: 'Unauthorized' });
  const expected = Buffer.from(`Bearer ${secret}`);
  const supplied = Buffer.from(header);
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const paths = req.body?.paths;
  if (
    !Array.isArray(paths) || paths.length === 0 || paths.length > 20 ||
    paths.some((path) => typeof path !== 'string' || !allowedPaths.has(path))
  ) {
    return res.status(400).json({ error: 'Supply 1-20 allowed paths' });
  }

  const completed = [];
  try {
    for (const path of new Set(paths)) {
      await res.revalidate(path);
      completed.push(path);
    }
    return res.status(200).json({ revalidated: completed });
  } catch {
    return res.status(500).json({ error: 'Revalidation failed', completed });
  }
}

After confirming fresh Delivery data, call the endpoint from a trusted worker or an operator's terminal. Load the same secret into that caller's environment:

ps
Invoke-RestMethod -Method Post `
  -Uri 'https://your-rendering-host.example/api/revalidate' `
  -Headers @{ Authorization = "Bearer $env:REVALIDATION_SECRET" } `
  -ContentType 'application/json' `
  -Body '{"paths":["/en/news/platform-update","/en/news"]}'

The paths body is our custom contract, not a Sitecore webhook payload. For automation, put an adapter or worker between the configured Edge webhook and this endpoint. Authenticate the incoming event, map changed items to affected routes, and retry failed deliveries with bounded backoff. Shared content may affect many pages; the two-path allowlist is deliberately small for this example.

Use a webhook execution mode appropriate to publish completion; Sitecore explains the options in Webhook execution modes. If fresh content is not yet observable, retry the readiness check before regeneration. Otherwise you can regenerate a page using an old response and cache it again.

Keep the application's existing Sitecore page-data factory and regeneration error handling. If it silently substitutes empty content after an upstream failure, regeneration can publish that empty result. This endpoint also needs deployment-level rate limits and an appropriate execution timeout before production use.

For App Router projects, use that router's path and data-cache APIs; res.revalidate() is a Pages Router API. Review App Router revalidation guidance against your installed Next.js version. Static exports require a rebuild and deployment because they have no ISR runtime.

4. Verify what visitors receive

Test against a production build or staging deployment with production caching behavior. Publish a distinctive value, confirm the probe matches, check the layout response, trigger revalidation, and inspect the public response body. Where available, Age, Cache-Control, x-vercel-cache, and x-nextjs-cache help identify cache behavior. A cache HIT alone says nothing about whether its content is current.

If the HTML has changed but the browser still shows the old value, inspect subsequent client requests and any service worker. A hard refresh can help isolate a browser issue, but cannot repair stale content upstream.

For new pages, verify route discovery and your application's fallback strategy. For deleted pages, verify that regeneration returns the intended 404 or redirect. An update workflow that works for existing URLs does not automatically cover either case.

The acceptance check is concrete: the intended field reaches Delivery, the intended page uses it, and the public response displays it. Keep those observations alongside publish and regeneration timestamps so the next stale-content incident starts with evidence.

    Share:
    Back to Blog

    Related Posts

    View All Posts »