How to Configure Next.js ISR Revalidation for Sanity

Incremental Static Regeneration (ISR) delivers static performance with dynamic update capabilities. Sanity.io provides real-time document mutations that require precise cache invalidation. This guide covers configuration patterns, webhook security, and edge-case resolution for production deployments.

1. ISR Architecture with Sanity: Time-Based vs On-Demand

Time-based revalidation polls the origin at fixed intervals. On-demand revalidation triggers instantly via webhooks. Sanity’s document lifecycle emits mutations that map directly to Next.js cache layers. Understanding this flow prevents stale content during editorial sprints. Review foundational Data Fetching & Caching Strategies before configuring route-level behavior.

DX Tradeoff: Time-based intervals guarantee eventual consistency but introduce latency. Webhooks provide instant updates but require strict payload validation and error handling.

// app-router-fetch-config.ts
import { createClient } from '@sanity/client';

export const sanity = createClient({
 projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
 dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
 useCdn: process.env.NODE_ENV === 'production',
 apiVersion: '2024-01-01',
});

// Critical: Always bind tags to Sanity _id fields for deterministic invalidation
export const fetchOptions = {
 next: { 
 tags: ['sanity-docs'],
 revalidate: 60 
 }
};
// sanity-client-setup.ts
// GROQ (Graph-Relational Object Queries) must return stable shapes
// Avoid dynamic query parameters that fragment cache keys
export const getDocumentQuery = (id: string) => `
 *[_type == "post" && _id == "${id}"][0]{
 _id, title, slug, body
 }
`;

2. Configuring Time-Based Revalidation in Next.js App Router

App Router defaults to force-cache. You must explicitly set revalidate to enable background regeneration. Use next: { revalidate: 60 } in fetch options. Structure Graph-Relational Object Queries (GROQ) to return deterministic payloads. Dynamic query parameters fragment cache keys. Bind tags to document _id fields instead.

// app/posts/[slug]/page.tsx
import { sanity, fetchOptions } from '@/lib/sanity-config';

export async function generateStaticParams() {
 // Pre-render known routes at build time
 return sanity.fetch(`*[_type == "post"]{ slug }`);
}

export default async function PostPage({ params }: { params: { slug: string } }) {
 // Critical: Explicit cache control overrides App Router defaults
 const data = await sanity.fetch(
 `*[_type == "post" && slug.current == "${params.slug}"][0]`,
 {},
 fetchOptions
 );

 return <article>{data.title}</article>;
}

3. Implementing On-Demand Revalidation via Sanity Webhooks

Configure Sanity Studio to POST mutation payloads to /api/revalidate. Verify payloads using Hash-based Message Authentication Code (HMAC) before invalidating. revalidateTag() offers deterministic cache clearing. revalidatePath() only matches exact route strings. Use tag-based routing for granular control. Advanced patterns are detailed in Implementing ISR with Next.js and Headless CMS.

// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
import { verifyHmac } from '@/lib/crypto';

export async function POST(req: NextRequest) {
 const signature = req.headers.get('x-sanity-signature');
 const body = await req.text();

 // Critical: Reject unverified payloads immediately
 if (!verifyHmac(signature, body, process.env.SANITY_WEBHOOK_SECRET!)) {
 return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
 }

 const payload = JSON.parse(body);
 const documentId = payload.documentId;

 // Tag-based invalidation scales better than path-based
 revalidateTag(`sanity-doc:${documentId}`);
 revalidateTag('sanity-docs');

 return NextResponse.json({ revalidated: true });
}
// lib/crypto.ts
import { createHmac, timingSafeEqual } from 'crypto';

export function verifyHmac(signature: string | null, payload: string, secret: string): boolean {
 if (!signature) return false;
 
 const expected = `sha256=${createHmac('sha256', secret).update(payload).digest('hex')}`;
 const actual = Buffer.from(signature);
 const expectedBuf = Buffer.from(expected);

 // Critical: Use timingSafeEqual to prevent brute-force attacks
 if (actual.length !== expectedBuf.length) return false;
 return timingSafeEqual(actual, expectedBuf);
}

4. Troubleshooting: Reproducible Stale Cache Scenarios

ISR failures often stem from header overrides or mismatched cache keys. Diagnose systematically.

Scenario A: Webhook Secret Mismatch & 401 Responses

Root cause: Missing or malformed x-sanity-signature header parsing. Resolution: Verify process.env.SANITY_WEBHOOK_SECRET alignment across environments. Implement strict HMAC validation before calling revalidateTag(). Return 401 immediately on mismatch.

Scenario B: Cache Key Collision with Dynamic Query Parameters

Root cause: Next.js caches identical URLs while Sanity returns variant data. Resolution: Normalize fetch URLs. Bind tags to Sanity _id fields. Avoid query string variations in cache keys.

Scenario C: Edge CDN Bypassing Next.js Cache

Root cause: Platform Time To Live (TTL) overrides Next.js defaults. Resolution: Explicitly set Cache-Control: public, s-maxage=1, stale-while-revalidate=59 in route responses. Align Content Delivery Network (CDN) TTL with ISR windows.

5. Prevention & Long-Term Maintenance

Automate cache health checks. Restrict real-time listeners to development environments. Document invalidation Service Level Agreements (SLAs) for editorial workflows.

  • Audit webhook delivery logs weekly
  • Align revalidate intervals with content update velocity
  • Monitor serverless function cold starts during mass invalidation

Pitfalls

Overusing revalidatePath(): Path invalidation requires exact string matches. It breaks when routes contain dynamic segments or middleware rewrites. Switch to tag-based invalidation for predictable behavior.

Ignoring s-maxage: Edge platforms cache responses independently of Next.js. A high s-maxage value bypasses origin revalidation entirely. Always set s-maxage equal to or slightly above your revalidate interval.

Unbounded Real-Time Listeners: Subscribing to Sanity listeners in production consumes memory and triggers race conditions with ISR. Use listeners only in development. Rely on webhooks for production cache updates.

FAQ

Q: Why does my page stay stale after a successful webhook 200 response? A: The CDN s-maxage header is likely overriding Next.js cache behavior. Audit route headers and purge edge nodes. Verify revalidateTag() targets the exact tag bound to your fetch call.

Q: Should I use time-based or on-demand revalidation? A: Use on-demand for editorial workflows requiring instant updates. Use time-based for high-traffic, low-change pages where webhook delivery guarantees are unreliable. Hybrid approaches work best.

Q: How do I handle mass document updates without triggering cold starts? A: Batch invalidations using a single shared tag. Schedule webhook retries during off-peak hours. Monitor serverless execution duration and adjust concurrency limits.