Sanity

Next.js App Router: On-Demand Revalidation and Live Preview with Sanity

Learn how to combine ISR, on-demand revalidation via Sanity webhooks, and live preview with the Sanity Presentation tool to build fast, always-fresh Next.js App Router sites.

June 26, 202610 min readMuhammad Zohaib Ramzan
Dark-themed code editor displaying Next.js App Router code with glowing syntax highlighting, representing ISR and on-demand revalidation with Sanity live preview

Why Content-Driven Sites Need Fast, Fresh Content

Modern content-driven sites face a fundamental tension: static pages are blazing fast, but they go stale the moment an editor hits "Publish" in the CMS. Server-rendered pages are always fresh, but they pay a latency cost on every request. Next.js solves this with Incremental Static Regeneration (ISR) — and when paired with Sanity's webhook system and Presentation tool, you get the best of all worlds: static-site performance, instant content updates, and a seamless live-preview editorial experience.

In this guide you'll learn how to wire up on-demand revalidation from Sanity to Next.js App Router, and how to enable the Sanity Presentation tool for live, in-context previews — all without leaving the App Router paradigm.

What Is ISR in the Next.js App Router?

Incremental Static Regeneration lets you statically generate pages at build time and then re-generate them in the background when they become stale. In the App Router, this is expressed through the fetch cache and the revalidate export.

Time-Based Revalidation

In any Server Component or fetch call you can set a time-based revalidation interval. At the route segment level, export a revalidate constant:

// app/posts/[slug]/page.tsx
export const revalidate = 60; // re-generate at most once per 60 seconds

Or control it per fetch call:

const data = await fetch('https://your-api.com/posts', { next: { revalidate: 60 } });

This is great for infrequently changing content, but a 60-second window is too slow for a busy editorial team. That's where on-demand revalidation comes in.

On-Demand Revalidation with Sanity Webhooks

On-demand revalidation lets you immediately purge and regenerate a cached page the moment content changes in Sanity — no waiting for a timer to expire.

The flow looks like this:

  1. An editor publishes or updates a document in Sanity Studio.
  2. Sanity fires a configured webhook to your Next.js app.
  3. Your Next.js API route calls revalidateTag() or revalidatePath() to bust the cache.
  4. The next visitor gets a freshly generated page.

Tagging Your Fetches

First, tag your Sanity data fetches so Next.js knows what to invalidate. Pass a tags array in the next option:

// lib/sanity.fetch.ts
import { client } from '@/sanity/client';

export async function getPost(slug: string) {
return client.fetch(
`*[_type == "post" && slug.current == $slug][0]`,
{ slug },
{ next: { tags: ['post', `post:${slug}`] } }
);
}

The Webhook Handler Route

Create a Next.js Route Handler at app/api/revalidate/route.ts. Use parseBody from next-sanity/webhook to verify the HMAC signature, then call revalidateTag:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { type NextRequest, NextResponse } from 'next/server';
import { parseBody } from 'next-sanity/webhook';

export async function POST(req: NextRequest) {
try {
const { isValidSignature, body } = await parseBody<{
_type: string;
slug?: { current: string };
}>(req, process.env.SANITY_REVALIDATE_SECRET);

if (!isValidSignature) {
return new NextResponse('Invalid signature', { status: 401 });
}

if (!body?._type) {
return new NextResponse('Bad Request', { status: 400 });
}

revalidateTag(body._type);

if (body.slug?.current) {
revalidateTag(`${body._type}:${body.slug.current}`);
}

return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
} catch (err) {
console.error(err);
return new NextResponse('Internal Server Error', { status: 500 });
}
}

Configuring the Sanity Webhook

In your Sanity project dashboard, go to API → Webhooks and create a new webhook with these settings:

  • URL: https://your-site.com/api/revalidate
  • Trigger on: Create, Update, Delete
  • Filter: _type == "post" (or leave blank for all types)
  • Secret: a strong random string stored in SANITY_REVALIDATE_SECRET

Sanity will include the secret in the sanity-webhook-signature header, which parseBody verifies automatically.

Live Preview with the Sanity Presentation Tool

On-demand revalidation keeps your published site fresh, but editors also need to preview unpublished drafts before hitting Publish. The Sanity Presentation tool provides a full visual editor experience — a side-by-side view of the Studio and your live site — powered by Next.js draftMode().

How It Works

  1. The Presentation tool opens your site in an iframe.
  2. It activates Next.js draftMode via a special route.
  3. Your Server Components detect Draft Mode and switch to fetching draft documents from the Sanity API using an authenticated token.
  4. Sanity's @sanity/preview-kit streams live updates as the editor types.

Installing Dependencies

npm install next-sanity @sanity/preview-kit

Enabling Draft Mode

Create a route at app/api/draft/route.ts that enables Draft Mode and redirects back to the previewed page:

// app/api/draft/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';

export async function GET(req: NextRequest) {
const { searchParams } = req.nextUrl;
const secret = searchParams.get('secret');
const slug = searchParams.get('slug') ?? '/';

if (secret !== process.env.SANITY_PREVIEW_SECRET) {
return new Response('Invalid token', { status: 401 });
}

draftMode().enable();
redirect(slug);
}

Also add a disable-draft route at app/api/disable-draft/route.ts so editors can exit preview mode:

// app/api/disable-draft/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export function GET() {
draftMode().disable();
redirect('/');
}

Fetching Drafts in Server Components

In your page component, check draftMode().isEnabled and use an authenticated Sanity client when active:

// app/posts/[slug]/page.tsx
import { draftMode } from 'next/headers';
import { getPost, getDraftPost } from '@/lib/sanity.fetch';

export default async function PostPage({ params }: { params: { slug: string } }) {
const { isEnabled } = draftMode();
const post = isEnabled
? await getDraftPost(params.slug) // uses token, bypasses CDN cache
: await getPost(params.slug); // uses cached, public data

return <PostContent post={post} />;
}

Your getDraftPost function should use a Sanity client configured with useCdn: false and a viewer/editor token, and pass cache: 'no-store' to prevent Next.js from caching draft responses.

Configuring the Presentation Tool in Sanity Studio

In your Studio configuration, add the presentationTool plugin:

// sanity.config.ts
import { defineConfig } from 'sanity';
import { presentationTool } from 'sanity/presentation';

export default defineConfig({
// ...
plugins: [
presentationTool({
previewUrl: {
origin: process.env.SANITY_STUDIO_PREVIEW_URL ?? 'http://localhost:3000',
draftMode: {
enable: '/api/draft',
},
},
}),
],
});

The Presentation tool will now appear as a tab in your Studio. When an editor opens it, it calls the draft route to enable Draft Mode, then renders your site in an iframe with live overlay annotations.

Enabling Visual Editing Overlays

For click-to-edit overlays, add the VisualEditing component from next-sanity to your root layout:

// app/layout.tsx
import { draftMode } from 'next/headers';
import { VisualEditing } from 'next-sanity';

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
{draftMode().isEnabled && <VisualEditing />}
</body>
</html>
);
}

This renders interactive overlays that let editors click any element on the page and jump directly to the corresponding field in Sanity Studio.

Why This Setup Is Ideal for Content-Driven Sites

Performance

Pages are statically generated and served from the CDN edge. On-demand revalidation means the cache is only busted when content actually changes — not on a fixed timer — so your cache hit rate stays high and your origin server stays quiet.

Editorial Experience

Editors get instant feedback through the Presentation tool's live preview. They can see exactly how a headline, image, or body copy change will look on the published site before committing. Visual editing overlays eliminate the guesswork of mapping Studio fields to rendered output.

Developer Experience

The entire setup lives in the App Router paradigm — Route Handlers, Server Components, and draftMode() from next/headers. There are no custom servers, no getServerSideProps, and no separate preview deployments. A single Next.js app handles both production traffic and editorial previews.

Scalability

Because revalidation is tag-based, you can be surgical about what you bust. Updating a single blog post only invalidates pages tagged with that post's slug — not your entire site. This keeps build times near zero and CDN efficiency near maximum.

Conclusion

Combining Next.js App Router ISR with Sanity's on-demand webhooks and the Presentation tool gives you a content architecture that is fast by default, fresh on demand, and delightful for editors. The key pieces are:

  • Tag your fetches with next: { tags: [...] } so you can invalidate precisely.
  • Verify webhook signatures in your Route Handler before calling revalidateTag.
  • Use Draft Mode to switch between cached public data and live draft data.
  • Add the Presentation plugin to Sanity Studio for a full visual editing experience.
  • Optionally add VisualEditing for click-to-edit overlays that bridge Studio fields and rendered output.

With these building blocks in place, your editorial team can publish with confidence and your users will always see the latest content — without sacrificing a millisecond of performance.