Next.js ISR Implementation
Incremental Static Regeneration serves pre-rendered HTML from the edge while regenerating individual pages in the background, so content updates ship without a full rebuild and without falling back to per-request SSR. For a headless CMS, that decouples deploys from publishes: editors get fresh content within a configurable window while readers still get static-fast TTFB. ISR sits at the center of most Data Fetching & Caching Strategies.
Revalidation mechanics
ISR runs on the revalidate threshold, in seconds. A request to an ISR route gets the cached HTML immediately. If more than revalidate seconds have passed since the last successful generation, Next.js queues a background regeneration — the current visitor still gets the cached page, the next one gets the rebuilt page.
The serve-stale-then-regenerate contract plays out per request like this:
flowchart TD
A["Request to ISR route"] --> B{"Page already generated?"}
B -->|no| C["fallback: blocking or true"]
C --> D["Fetch CMS + generate, cache HTML"]
D --> E["Serve page"]
B -->|yes| F{"revalidate window elapsed?"}
F -->|no| G["Serve cached HTML"]
F -->|yes| H["Serve cached HTML to this visitor"]
H --> I["Queue background regeneration"]
I --> J["Next visitor gets rebuilt page"]
The cache boundary lives at the framework and CDN layer, which is the same serve-stale-then-refresh contract as SWR Stale-While-Revalidate Patterns, just not in browser memory.
Two exports drive it: getStaticPaths enumerates routes, getStaticProps hydrates data. revalidate is the tuning knob — too low raises origin load and regeneration collisions, too high delays editorial updates. A 60–300s window works for most content sites. Next.js respects standard HTTP caching, so edge networks serve cached responses until regeneration completes (MDN: HTTP Caching).
Implementation
Dynamic CMS routing with typed props and ISR:
// pages/articles/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';
import { fetchArticleBySlug, fetchAllArticleSlugs } from '@/lib/cms-client';
interface Article {
id: string;
slug: string;
title: string;
publishedAt: string;
bodyHtml: string;
}
interface ArticlePageProps {
article: Article | null;
}
export default function ArticlePage({ article }: ArticlePageProps) {
if (!article) {
return <div className="error-state p-8 text-center">Article unavailable</div>;
}
return (
<article className="prose max-w-3xl mx-auto px-4 py-12">
<h1>{article.title}</h1>
<p className="text-gray-500 text-sm">Published: {article.publishedAt}</p>
<div dangerouslySetInnerHTML={{ __html: article.bodyHtml }} />
</article>
);
}
export const getStaticPaths: GetStaticPaths = async () => {
const slugs = await fetchAllArticleSlugs();
return {
// Pre-build only high-traffic or recently published content
paths: slugs.slice(0, 50).map(slug => ({ params: { slug } })),
fallback: 'blocking',
};
};
export const getStaticProps: GetStaticProps<ArticlePageProps> = async ({ params }) => {
const slug = params?.slug as string;
const article = await fetchArticleBySlug(slug);
if (!article) {
return { notFound: true };
}
return {
props: { article },
// Regenerate at most once every 60 seconds per route
revalidate: 60,
};
};
Fallback behavior
The fallback value in getStaticPaths controls requests for routes not pre-built. fallback: 'blocking' makes the server wait for the CMS fetch and generation before responding — no loading skeleton, but higher TTFB on uncached routes. fallback: true returns a shell immediately with a loading state, better for high-traffic sites but it requires careful hydration handling. With fallback: true, article arrives as null on first render, so you need a guard that shows a loading UI until regeneration completes and Next.js re-renders with data. Structuring those states is covered in resolving Next.js ISR fallback pages for missing CMS content.
Revalidation tuning and CDN sync
ISR depends on tight sync between the Next.js server, the CMS, and the edge CDN. When revalidate expires, regeneration happens on origin — but distributed CDNs keep serving stale edge caches until their own TTL expires or an explicit purge arrives. Time-based revalidation alone leaves a gap between CMS publish and edge visibility. Closing it means webhook-driven on-demand revalidation via /api/revalidate; coordinating these layers is detailed in handling cache invalidation across distributed CDNs.
ISR only covers server-rendered routes. For client-side mutations, interactive dashboards, or comment threads, layer a client cache: React Query for CMS Data handles optimistic updates and background sync without disturbing ISR’s static baseline.
Production checklist
- Tune
revalidateper route type: 30–60s for news feeds, 3600s+ for evergreen docs. - Wire CMS webhooks to
/api/revalidateso publishes update the edge immediately instead of waiting on the time-based cycle. - Watch for regeneration collisions: track
x-nextjs-cache: REVALIDATING; if collisions spike, raiserevalidateor deduplicate at the CDN. - Load-test uncached routes so
fallback: 'blocking'doesn’t exhaust serverless execution limits. - Sanitize CMS payloads: validate
dangerouslySetInnerHTMLcontent or use a strict Markdown parser to prevent XSS in statically cached pages.