RMC Digital Logo
Sitecore,  SitecoreAI

Revalidating Cache in Next.js with Sitecore Publish Webhooks

Author

Roberto Armas

Date Published


One of the nicest improvements in the modern Next.js caching story is how much easier it is to invalidate content deliberately instead of rebuilding everything.

With Cache Components, you can cache more than fetch() calls. You can cache arbitrary server work with "use cache" and tag that work with cacheTag(). Then, when content changes, you can invalidate only the parts that matter with revalidateTag(). This is especially useful in Sitecore-driven solutions, where published content should show up quickly without forcing a full-site rebuild.

Why this matters

Traditionally, many headless implementations leaned on full redeploys or broad cache purges after publishing. That works, but it is expensive and blunt.

With Next.js tag-based revalidation, the flow becomes much cleaner:

  • Cache your content logic with "use cache".
  • Tag that cached work with something meaningful, such as sitecore-content.
  • When Sitecore publishes, call a secure endpoint in your app.
  • That endpoint runs revalidateTag("sitecore-content", "max").
  • The next request serves stale content immediately and refreshes in the background using stale-while-revalidate behavior

That gives you a strong middle ground: fast responses, selective invalidation, and no unnecessary rebuilds.

Cache Components + Sitecore

This is where Cache Components fits really well in Sitecore projects.

Next.js now lets you use cacheTag() inside a "use cache" scope, which means you are no longer limited to tagging only fetch() calls. You can tag computed layout data, route helpers, CMS transformations, and other server-side work that sits between Sitecore and your rendered page.

A simple example:

1import { cacheTag } from "next/cache";
2
3export async function getGraphqlData(params: any) {
4 "use cache";
5
6 cacheTag("sitecore-content");
7
8 // fetch data using Preview API / Delivery API
9 return await sitecoreClient.getMyBeautifulCachedData(params);
10}

The important part is consistency: every cached helper that depends on published Sitecore content should include the same tag, or a well-planned tag taxonomy.

At minimum, make sure you include:

1cacheTag("sitecore-content");

That gives you a shared invalidation hook across the application.

Create a secure revalidation endpoint

Next.js supports revalidateTag() in Route Handlers and Server Functions, so a route under /app/api/revalidate/route.ts is a good fit

Here is a solid starting point:

1import { NextRequest, NextResponse } from "next/server";
2import { revalidateTag } from "next/cache";
3import debug from "src/debug";
4
5export async function POST(req: NextRequest) {
6 try {
7 const body = (await req.json()) as { tags?: string[] };
8 const revalidateTagToken = req.headers.get("x-revalidate-token");
9
10 if (
11 !revalidateTagToken ||
12 process.env.REVALIDATE_TAG_TOKEN !== revalidateTagToken
13 ) {
14 debug.revalidate("Invalid revalidate tag token.");
15 return NextResponse.json(
16 { error: "Unauthorized request." },
17 { status: 401 }
18 );
19 }
20
21 const tags =
22 Array.isArray(body.tags)
23 ? body.tags.filter((t): t is string => typeof t === "string" && !!t.trim())
24 : typeof body.tag === "string" && body.tag.trim()
25 ? [body.tag.trim()]
26 : [];
27
28 if (!tags.length) {
29 debug.revalidate("Invalid tag payload.");
30 return NextResponse.json(
31 { error: "Expected tag or tags in request body." },
32 { status: 400 }
33 );
34 }
35
36 for (const tag of tags) {
37 revalidateTag(tag, "max");
38 debug.info("Revalidated tag: %s", tag);
39 }
40
41 return NextResponse.json({
42 ok: true,
43 revalidated: true,
44 tags,
45 });
46 } catch {
47 return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 });
48 }
49}

Always protect this endpoint

This part is not optional.

Your revalidation endpoint must always be protected. If left open, anyone who discovers it could trigger cache invalidation on demand.

A simple shared secret in a custom header is often enough for this use case, as long as it is stored in environment variables and never hardcoded into the repository.

That is exactly what the example above does with

1const revalidateTagToken = req.headers.get("x-revalidate-token");

and .env variable:

1process.env.REVALIDATE_TAG_TOKEN

Also worth noting: revalidateTag() only works in server environments such as Route Handlers and Server Functions, not in Client Components

Register a Sitecore publish webhook

On the Sitecore side, you can register an Admin Webhook so that your app is notified after publishing completes.

Sitecore’s webhook execution modes include OnEnd, which runs at the end of the publishing job and is the default option. This makes it a natural fit for cache invalidation after content is published. If a body is included, Sitecore sends it as the webhook request body.

Example:

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

Payload:

1{
2 "label": "Revalidate Cache On Publish Webhook",
3 "uri": "https://[your-vercel-endpoint]/api/revalidate",
4 "method": "POST",
5 "headers": {
6 "x-revalidate-token": "[Your-Beautiful-Secret]"
7 },
8 "body": "{\"tags\": [\"sitecore-content\"]}",
9 "executionMode": "OnEnd",
10 "createdBy": "john.doe@sitecore.com"
11}

This is a nice pattern because publish completion in Sitecore becomes the trigger for targeted cache invalidation in Next.js.

Why revalidateTag(..., "max") is a good default

In current Next.js guidance, revalidateTag(tag, "max") is the recommended pattern because it uses stale-while-revalidate semantics. That means users can still get a fast response while the fresh content is loaded in the background.

Suggested tag strategy

Use one shared tag for all published CMS content.

1cacheTag("sitecore-content");

This is the fastest way to get started, and it works well for many teams. As your app grows, add more specific tags for content domains.

1cacheTag("sitecore-content", "products", "blogs", ... etc);

That gives you the option to invalidate selectively later, while still keeping the broader fallback tag

Conclusion

Tag-based revalidation is one of the most practical upgrades in the modern Next.js + Sitecore stack.

With Cache Components, you can cache more of your Sitecore integration layer. With cacheTag(), you can label those cached units intentionally. And with a protected revalidation endpoint plus a Sitecore OnEnd publish webhook, you get a clean publish-to-refresh pipeline without resorting to full rebuilds.

The nice part is that this approach is incremental. You do not need to redesign the whole application in one pass. Start with page-level cached helpers and a single shared tag like sitecore-content. Then, over time, evolve toward more granular tagging where it actually adds value.

I hope Sitecore evolves this story further and eventually gives us more granular control over cache invalidation patterns and cache tags out of the box. That would make it even easier to target exactly the content that changed instead of relying on broader revalidation strategies. Still, even with the current approach, this solution gives teams a practical and reliable way to revalidate content whenever authors publish changes, without needing to fall back to full rebuilds or heavier deployment workflows.