---
title: How to revalidate Sitecore pages on publish with Next.js
description: Learn how to use a Sitecore Experience Edge webhook and Next.js revalidatePath to refresh cached Sitecore Content SDK pages after each publish.
url: /kb/guide/update-sitecore-content-manually
canonical_url: "https://vercel.com/kb/guide/update-sitecore-content-manually"
published: 2026-09-09
last_updated: 2026-09-09
authors: Pat Beecher
related:
  - /docs/incremental-static-regeneration/limits-and-pricing
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Use a Sitecore Experience Edge `OnUpdate` webhook to refresh cached pages after content is published in a Next.js App Router app using Sitecore Content SDK. This setup uses `revalidatePath('/', 'layout')` and does not use Cache Components, cache tags, or data source dependency mapping.

Use this approach when your Next.js app uses static rendering or ISR in production and you want publish-driven invalidation instead of a short time-based interval.

## Overview

In this guide, you'll:

- Add a protected `/api/revalidate` Route Handler to a Sitecore Content SDK app.
  
- Register a Sitecore Experience Edge webhook using the Admin API.
  
- Test the endpoint locally and against a deployed HTTPS URL.
  
- Understand the tradeoff between broad on-demand revalidation and time-based revalidation.
  

## Prerequisites

- A Next.js App Router app created from the [Sitecore Content SDK](https://doc.sitecore.com/sai/en/developers/content-sdk/20/getting-started-with-next-js-app-router-using-content-sdk.html) [`nextjs-app-router`](https://doc.sitecore.com/sai/en/developers/content-sdk/20/getting-started-with-next-js-app-router-using-content-sdk.html) [template.](https://doc.sitecore.com/sai/en/developers/content-sdk/20/getting-started-with-next-js-app-router-using-content-sdk.html)
  
- Static rendering or ISR for the Sitecore routes you want to refresh.
  
- Sitecore Experience Edge administration credentials.
  
- `curl` and `jq` installed in the CLI.
  
- A deployed HTTPS URL, or an HTTPS tunnel (`ngrok` or `tailscale`) for local webhook testing.
  

## How it works

Sitecore sends an `OnUpdate` webhook after publishing content to the Experience Edge. The webhook calls a Next.js Route Handler at `/api/revalidate`, and the handler revalidates the pages using:

```typescript
revalidatePath('/', 'layout');
```

In the Content SDK App Router template, Sitecore pages live under the app's layout tree. Revalidating the root layout marks that tree stale, including page routes, locales, multisite paths, navigation, shared components, and reused data source items.

The call does not render every page during the webhook request. The next request to a cached page regenerates that page.

## Steps

### 1\. Add an environment variable

The Sitecore webhook sends a secret value in the `x-revalidate-secret` header.

Generate a random value with:

```bash
openssl rand -hex 32
```

Add this variable locally in the `.env.local` and in the deployed environment:

```bash
SITECORE_REVALIDATE_SECRET=your_revalidate_secret_here
```

### 2\. Add the webhook Route Handler

Create a new `src/app/api/revalidate/route.ts` file:

```typescript
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';

const SECRET_HEADER = 'x-revalidate-secret';

type SitecoreWebhookUpdate = {
  identifier?: string;
  entity_definition?: string;
  operation?: string;
  entity_culture?: string;
};

type SitecoreWebhookBody = {
  invocation_id?: string;
  updates?: SitecoreWebhookUpdate[];
  continues?: boolean;
};

export async function POST(request: Request) {
  const configuredSecret = process.env.SITECORE_REVALIDATE_SECRET?.trim();

  if (!configuredSecret) {
    return NextResponse.json(
      { error: 'SITECORE_REVALIDATE_SECRET is not configured.' },
      { status: 500 }
    );
  }

  if (request.headers.get(SECRET_HEADER) !== configuredSecret) {
    return NextResponse.json({ error: 'Unauthorized.' }, { status: 401 });
  }

  let body: SitecoreWebhookBody;

  try {
    body = await request.json();
  } catch {
    return NextResponse.json(
      { error: 'Request body must be valid JSON.' },
      { status: 400 }
    );
  }

  if (typeof body !== 'object' || body === null || Array.isArray(body)) {
    return NextResponse.json(
      { error: 'Request body must be a JSON object.' },
      { status: 400 }
    );
  }

  const updates = Array.isArray(body.updates) ? body.updates : [];

  revalidatePath('/', 'layout');

  return NextResponse.json({
    revalidated: true,
    path: '/',
    type: 'layout',
    invocation_id: body.invocation_id ?? null,
    updates: updates.length,
    continues: body.continues ?? false,
  });
}
```

If your Sitecore routes live under a different shared layout, pass that layout path instead of `/`.

### 3\. Create the Experience Edge webhook

To register the webhook with the Sitecore Experience Edge, first generate the client ID and secret to request the Sitecore APIs:

1. In the Sitecore Cloud Portal, open Stream.
   
2. Click **Admin** > **AI API keys** > **Create credential**.
   
3. In the **​Create New Client​​** dialog, enter a name and description for the client. Then click **Create**. The ​Client ID and Client Secret​​ display.
   
4. Copy the Client ID and Client Secret because you won't be able to view them again in Stream. You'll use them to request an access token.
   

With the client ID and secret, generate a JSON Web Token (JWT) using the API:

```bash
CLIENT_ID=your_client_id_here
CLIENT_SECRET=your_client_secret_here

TOKEN=$(
  curl -sS -X POST 'https://auth.sitecorecloud.io/oauth/token' \
    -H 'Content-Type: application/json' \
    --data-raw "{
      \"audience\": \"https://api.sitecorecloud.io\",
      \"grant_type\": \"client_credentials\",
      \"client_id\": \"$CLIENT_ID\",
      \"client_secret\": \"$CLIENT_SECRET\"
    }" |
    jq -r '.access_token'
)

echo "${TOKEN:0:20}..."
```

List existing webhooks before creating another one:

```bash
curl -sS 'https://edge.sitecorecloud.io/api/admin/v1/webhooks' \
  -H "Authorization: Bearer $TOKEN" |
  jq
```

Create the `OnUpdate` webhook:

```bash
REVALIDATE_URL='https://your-domain.com/api/revalidate'
SITECORE_REVALIDATE_SECRET='your_revalidate_secret_here'

curl -sS -X POST 'https://edge.sitecorecloud.io/api/admin/v1/webhooks' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d "$(
    jq -n \
      --arg uri "$REVALIDATE_URL" \
      --arg secret "$SITECORE_REVALIDATE_SECRET" \
      '{
        label: "Next.js broad revalidatePath",
        uri: $uri,
        method: "POST",
        headers: {
          "x-revalidate-secret": $secret
        },
        createdBy: "your_name_here",
        executionMode: "OnUpdate"
      }'
  )" |
  jq
```

Use `OnUpdate` because Sitecore sends a JSON body with `invocation_id`, `updates[]`, and `continues`. The handler does not need the item IDs for routing, but it’s useful when debugging to confirm the webhook fired.

### 4\. Test locally

Build and run the production server. Development mode does not prove the ISR behavior.

```bash
npm run build
npm run start
```

Test the secret by sending a request without the secret:

```bash
curl -i -X POST 'http://localhost:3000/api/revalidate' \
  -H 'Content-Type: application/json' \
  -d '{}'
```

It should get rejected with the expected response:

```txt
HTTP/1.1 401 Unauthorized
```

Test a valid webhook request:

```bash
curl -sS -X POST 'http://localhost:3000/api/revalidate' \
  -H 'Content-Type: application/json' \
  -H "x-revalidate-secret: $SITECORE_REVALIDATE_SECRET" \
  -d '{
    "invocation_id": "local-test",
    "updates": [
      {
        "identifier": "2B582500DC2242B7B7BCB313D10889DC",
        "entity_definition": "Item",
        "operation": "Update",
        "entity_culture": "en"
      }
    ],
    "continues": false
  }' |
  jq
```

Expected response:

```json
{
  "revalidated": true,
  "path": "/",
  "type": "layout",
  "invocation_id": "local-test",
  "updates": 1,
  "continues": false
}
```

Test the deployed endpoint with the same request:

```bash
curl -sS -X POST 'https://your-domain.com/api/revalidate' \
  -H 'Content-Type: application/json' \
  -H "x-revalidate-secret: $SITECORE_REVALIDATE_SECRET" \
  -d '{
    "invocation_id": "deployed-test",
    "updates": [],
    "continues": false
  }' |
  jq
```

To test a real Sitecore webhook locally, expose your local server with an HTTPS tunnel and set `REVALIDATE_URL` to the tunnel URL before creating the webhook.

## How this compares with time-based revalidation

Broad on-demand revalidation is usually a better starting point than a short time-based interval, such as 5s. Sitecore publish actions are content events, so the cache should be invalidated when the event occurs rather than expiring every few seconds when the content has not changed.

For a 10,000-page site, `revalidatePath('/', 'layout')` does not regenerate 10,000 pages immediately during the webhook request. It marks the layout tree stale. Pages regenerate the next time they are requested.

Time-based revalidation also works per request. If a page has a 5s revalidation interval, the first request after the interval can serve stale content and trigger background regeneration. On high-traffic pages, that can happen repeatedly even when Sitecore content has not changed. On pages with no traffic, nothing regenerates until a request arrives.

The tradeoff is the cache hit rate after publication. Broad on-demand revalidation can make many cached pages stale at once, so popular pages regenerate after each publish. If the site publishes often and has high traffic, move toward targeted `revalidatePath` calls, tag-based revalidation, or a page-to-datasource dependency index.

## Troubleshooting

- If pages never change, confirm that the routes are static or that ISR is enabled in the production build. `revalidatePath` does not refresh fully dynamic pages.
  
- If the endpoint returns `401`, the webhook header does not match the `SITECORE_REVALIDATE_SECRET` value. Ensure that you’ve set the correct value in the Vercel project’s environment settings.
  
- If the endpoint returns `400`, the body is not valid JSON.
  
- If the endpoint returns `500`, set `SITECORE_REVALIDATE_SECRET` in the environment. Ensure that you’ve set the correct value in the Vercel project’s environment settings. Also, confirm you’ve set the value in the correct environment (e.g. Preview or Production).
  
- If Sitecore stops calling the endpoint, inspect the webhook with `GET /webhooks/{id}` and check `lastRuns`.
  
- If the webhook is disabled, fix the endpoint and re-enable it. Sitecore disables a webhook after 10 consecutive failures, and a request that takes longer than 30s counts as a failure.
  

## Next steps

- Read the [Next.js](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) [reference](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) to learn more.
  
- Explore [Next.js caching without Cache Components](https://nextjs.org/docs/app/guides/caching-without-cache-components).
  
- Learn about the cost impact in [Vercel’s ISR usage and pricing](https://vercel.com/docs/incremental-static-regeneration/limits-and-pricing).
  
- Review the [Sitecore Experience Edge Admin API](https://doc.sitecore.com/xp/en/developers/hd/22/sitecore-headless-development/admin-api.html).
  
- See the [Sitecore webhook objects](https://doc.sitecore.com/xp/en/developers/hd/19/sitecore-headless-development/webhook-objects.html) to learn about the webhook fields.