No More Delays: How to Instantly Reflect Sitecore XM Cloud Changes on Netlify & Vercel

 

Tired of waiting for your freshly published Sitecore content to show up on your Netlify-hosted site or Vercel -hosted site? You’re not alone—and the good news is, the wait ends here. In this blog, I’ll walk you through a simple and effective solution to ensure your updates go live instantly. Follow along, and by the end, you’ll not only solve the problem—you might just feel like sending a virtual high five.

Objective

To ensure that any content published from Sitecore XM Cloud is immediately reflected on the Netlify-hosted or Vercel-hosted Next.js frontend by triggering on-demand revalidation.

Prerequisites

·       - Sitecore XM Cloud environment with publishing webhook support

·       - Next.js frontend hosted on Netlify

·       - Access to create and configure environment variables

·       - GraphQL API and publishing API credentials from Sitecore XM Cloud

Files Involved

1. `revalidate.ts` – API route in Next.js to revalidate OSR pages

2. `webhook.ts` – API route to handle incoming webhooks from Sitecore and trigger revalidation

Step-by-Step Setup

1. Create and expose `/api/revalidate` endpoint

·       - Accepts a secret and a list of URLs to revalidate

·       - Performs revalidation for each URL




import type { NextApiRequest, NextApiResponse } from 'next';

export default handler;

export interface RevalidateRequest {

url?: string[];

secret?: string;

}

async function handler(req: NextApiRequest, res: NextApiResponse) {

const revalidateRequest = req.body as RevalidateRequest;

if (revalidateRequest.secret !== process.env.REVALIDATE_TOKEN) {

return res.status(401).json({ revalidated: false, error: 'Invalid secret' });

}

if (!revalidateRequest?.url?.length) {

return res.status(400).json({ revalidated: false, error: 'No URL provided' });

}


try {

const succededUrls = [];

const failedUrls = [];

const skippedUrls = [];

// Process URLs sequentially instead of in parallel to avoid potential race conditions

for (const url of revalidateRequest.url) {

let retries = 3;

let success = false;

let lastError: Error | null = null;

if (!url) {

skippedUrls.push({

url: url,

error: 'No URL provided',

});

continue;

}

// Normalize the path to ensure consistent handling

const normalizedUrl = url?.startsWith('/') ? url : `/${url}`;

console.log(`Attempting to revalidate: ${normalizedUrl}`);

while (retries > 0 && !success) {

try {

// Add a small delay between retries to prevent overwhelming the system

if (retries < 3) {

await new Promise((resolve) => setTimeout(resolve, 1000));

}

await res.revalidate(normalizedUrl);

success = true;

succededUrls.push(normalizedUrl);

console.log(`Successfully revalidated: ${normalizedUrl}`);

} catch (error: unknown) {

lastError = error instanceof Error ? error : new Error(String(error));

console.error(`Revalidation attempt failed for ${normalizedUrl}: ${lastError.message}`);

retries--;

}

}

if (!success) {

failedUrls.push({

url: normalizedUrl,

error: lastError?.message || 'Unknown error',

});

}
}
return res.status(200).json({

revalidated: failedUrls.length === 0,

successRatio: `${(succededUrls.length / revalidateRequest.url.length) * 100}%`,

items: revalidateRequest.url.length,

succededUrls,

failedUrls,

skippedUrls,

});

} catch (error: unknown) {

const err = error instanceof Error ? error : new Error(String(error));

console.log(JSON.stringify(req.body, null, 2));

console.error('Revalidation process failed:', err);

return res.status(500).json({

revalidated: false,

error: err.message || 'Revalidation failed',

});

}
}

2. Create `/api/webhook` endpoint

·       - Receives webhook calls from Sitecore

·       - Filters published items that have layouts

·       - Fetches the updated page URLs using Sitecore GraphQL

·       - Calls the `/api/revalidate` endpoint with those URLs
     


// import { GraphQLRequestClient } from '@sitecore-jss/sitecore-jss-nextjs/graphql';

import axios from 'axios';

import { GetItemUrl } from 'lib/webhook/revalidate/graphql';

import {

SitecoreItemUrl,

TGetItemUrlRoot,

TUrl,

WebhookRequestBody,

} from 'lib/webhook/revalidate/type';

import type { NextApiRequest, NextApiResponse } from 'next';

import config from 'temp/config';

async function fetchItemUrl(itemId: string, lang: string): Promise<TUrl> {

const data = (await fetch(process.env.GRAPH_QL_ENDPOINT_MASTER || '', {

method: 'POST',

headers: {

'Content-Type': 'application/json',

sc_apikey: process.env.SITECORE_API_KEY_MASTER || '',

},

body: JSON.stringify({ query: GetItemUrl, variables: { id: itemId, lang } }),

})

.then((res) => res.json())

.then((res) => res.data)) as TGetItemUrlRoot;

console.log('mydata', data);

if (data?.item?.url) {

return {

...data?.item?.url,

path: getIsrUrl(`/${lang}${data?.item?.url?.path?.toLowerCase()}`),

};

}
return {} as TUrl;

}

function getIsrUrl(url: string): string {

// Split the path into segments

const segments = url?.split('/') || [];

// Check if first segment is a locale (with or without region code)

if (segments[1] && /^([a-z]{2}(-[a-z]{2})?)?$/.test(segments[1])) {

// Insert 'gwlg' after the locale segment

segments.splice(2, 0, `_site_${config.sitecoreSiteName}`);

// Join segments back together

return segments.join('/');

}

return url;

}

/**

* Handles the webhook request.

* @param req - The NextApiRequest object.

* @param res - The NextApiResponse object.

*/

export default async function handler(req: NextApiRequest, res: NextApiResponse) {

if (req.method === 'POST') {

try {

// Check if the onUpdate webhook is enabled

if (process.env.PUBLISHING_WEBHOOK_ENABLED === 'false') {

res.status(200).end('ONUPDHOOK is disabled');

return;

}

const { updates } = req.body as WebhookRequestBody;

console.log('updates', updates);

// Filter out the LayoutData updates (Items that has layout), and get the item URL for each item page

const layoutDataUpdates: SitecoreItemUrl[] = updates

.filter((update) => update.identifier.includes('-layout'))

.map(({ identifier, entity_culture }) => ({ identifier, entity_culture }));

console.log('ONUPDHOOK LayoutData updates', layoutDataUpdates);

const urls: TUrl[] = await Promise.all(

layoutDataUpdates.map(async ({ identifier, entity_culture }): Promise<TUrl> => {

return fetchItemUrl(identifier.replace('-layout', ''), entity_culture);

})

);

console.log('ONUPDHOOK URLs that needs to be revalidated', urls);

// for each Url for a page which it's Layout been updated on edge, Revalidate the URLs in Vercel cache


let headers: { [key: string]: string } = { 'Content-Type': 'application/json' };

console.log(

'ONUPDHOOK VERCEL_BYPASS_KEY is available',

process.env.VERCEL_BYPASS_KEY && process.env.VERCEL_BYPASS_KEY !== ''

);

if (process.env.VERCEL_BYPASS_KEY && process.env.VERCEL_BYPASS_KEY !== '') {

console.log('ONUPDHOOK Applying Vercel bypass header', urls);

headers = {

'Content-Type': 'application/json',

'x-vercel-protection-bypass': process.env.VERCEL_BYPASS_KEY ?? '',

};

}

const revalidationUrls = urls.map((url) => url.path).filter((url) => url !== '');

console.log('revalidationUrls', revalidationUrls);

const revalidateResponse = await axios.post(

`${process.env.NEXT_PUBLIC_URL}/api/revalidate`,

{

secret: process.env.REVALIDATE_TOKEN,

url: revalidationUrls,

},

{

headers,

timeout: process.env.WEBHOOK_TIMEOUT ? parseInt(process.env.WEBHOOK_TIMEOUT) : 800000,

validateStatus: (status) => status < 500,

}

);

const response = {

message: `ONUPDHOOK revalidated all these ${urls.length} URLs`,

urls: revalidationUrls,

info: revalidateResponse.data,

};

console.log('ONUPDHOOK Response', JSON.stringify(response));

res.status(200).json(response);

} catch (error) {

console.error('ONUPDHOOK catch' + error);

//we need to respond with 200 to avoid retries from webhook invocation

res

.status(200)

.json({ message: 'Webhook processing failed', reasons: JSON.stringify(error, null, 2) });

}

} else {

// Handle any other HTTP method

res.setHeader('Allow', ['POST']);

res.status(405).end(`Method ${req.method} Not Allowed`);

}

}

3. Register a Webhook in XM Cloud

To ensure content publishing triggers revalidation, you need to register a webhook in XM Cloud. Follow the steps below:

          a.Generate environment-specific Client ID and Client Secret

    • Make a POST request to the following URL to generate the bearer token:

  https://auth.sitecorecloud.io/oauth/token

    • Pass the Client ID and Client Secret in the request body.

Providing sample CURL here


curl --location 'https://auth.sitecorecloud.io/oauth/token' \

--header 'content-type: application/x-www-form-urlencoded' \

--header 'Cookie: __cf_bm=87UHNA4xv1frSeLI5t.3bI5N4M7w9eTah4L1nrsq1vA-1749193101-1.0.1.1-v9PNOX6UUj79DWYcO8jYEaqHdmk0LWbXarV_kVXFbshHqpuNfIMa4vYfuGZrqsLT; did=s%3Av0%3A96d84226-38b6-4996-b955-f1c10b81601c.0jKdACqeKjWe%2BGVb7IOz%2Bjj7SZPGQv8nMDmWVGJI%2BGU; did_compat=s%3Av0%3A96d84226-38b6-4996-b955-f1c10b81601c.0jKdACqeKjWe%2BGVb7IOz%2Bjj7SZPGQv8nMDmWVGJI%2BGU' \

--data-urlencode 'audience=https://api.sitecorecloud.io' \

--data-urlencode 'grant_type=client_credentials' \

--data-urlencode 'client_id=<Client ID>' \

--data-urlencode 'client_secret=<Client Secret>'

 

b. Register the Webhook

    • Once you have the bearer token, make a POST request to the following endpoint to register the webhook:

     https://edge.sitecorecloud.io/api/admin/v1/webhooks

    • The payload should specify your backend’s /api/webhook endpoint where the webhook notifications will be sent.

Change the Label, uri, createdBy, executionMode as per the requirement for other urls if you want to purge specific to any other URL.

Providing sample CURL here:

curl --location 'https://edge.sitecorecloud.io/api/admin/v1/webhooks' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <Add Bearer Token here>' \
--data '{
     "label": "Behavioung Data",
     "uri": "
<Website domain url- Netlify>/api/webhook",
     "method": "GET",
     "createdBy": "Mahesh Kambhampati",
     "executionMode": "OnUpdate"
}'

 

4. Deploy the Changes to Netlify

·       - Ensure your API routes (`/api/webhook`, `/api/revalidate`) are deployed

·       - Confirm that environment variables are correctly set in Netlify

5. Test the Workflow

·       - Publish a content item with a layout in Sitecore

·       - Verify the webhook is triggered and the content is updated immediately on the Netlify site

Outcome

With the above setup, published content from Sitecore will:

·       - Trigger a webhook call to your backend

·       - Resolve the updated URLs

·       - Revalidate those pages instantly on Netlify

·       - Avoid stale content and eliminate delay from edge cache propagation

Hope this blog helped you solve the mystery of the missing updates. If it did, go ahead—do a little happy dance ๐Ÿ’ƒ๐Ÿ•บ (or just drop a like, share, or a comment). Until next time, may your deploys be smooth and your cache forever fresh!

Comments

Post a Comment

Popular posts from this blog

Optimizing Layout Service: Exposing Sitecore Search Configurations for Each Page in XM Cloud

Implementing Blog Carousel Personalization using Sitecore Personalize and Decision Models