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
- Follow the official Sitecore documentation to
create credentials:
Create an Edge Administration Client for an XM Cloud Environment - 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
insightful
ReplyDelete