Incremental Static Regeneration (ISR)

Incremental Static Regeneration (ISR) lets you build static pages at deploy time and update them without a full rebuild. When combined with the Uniform Page Router SDK, ISR gives you the performance benefits of static pages with the content freshness of server-side rendering.

ISR is configured through the standard Next.js getStaticProps and getStaticPaths functions, which the Uniform SDK wraps with withUniformGetStaticProps and withUniformGetStaticPaths.


  1. At build time, getStaticPaths fetches all paths from the Uniform Project Map. getStaticProps resolves each composition via the Route API and pre-renders the page.

  2. At runtime, cached pages are served instantly from the CDN. Pages not pre-rendered at build time (or new pages added after the build) are rendered on their first request and cached (because fallback: true).

  3. When content changes, pages are revalidated either on a time interval (revalidate option) or on-demand via a webhook from Uniform.


// pages/[[...slug]].tsx import PageComposition from "@/components/PageComposition"; import { withUniformGetStaticProps, withUniformGetStaticPaths, } from "@uniformdev/canvas-next/route"; import { CANVAS_DRAFT_STATE, CANVAS_PUBLISHED_STATE, } from "@uniformdev/canvas"; export const getStaticProps = withUniformGetStaticProps({ requestOptions: { state: process.env.NODE_ENV === "development" ? CANVAS_DRAFT_STATE : CANVAS_PUBLISHED_STATE, }, param: "slug", }); export const getStaticPaths = withUniformGetStaticPaths(); export default PageComposition;

By default, withUniformGetStaticPaths returns { fallback: true }, which means:

  • Pages listed in the paths array are pre-rendered at build time.
  • Pages not in the list are rendered on their first request and then cached.
  • This is the recommended default -- it ensures new pages work immediately without requiring a rebuild.

To periodically refresh static pages, use handleComposition to add a revalidate value:

export const getStaticProps = withUniformGetStaticProps({ requestOptions: { state: CANVAS_PUBLISHED_STATE, }, param: "slug", handleComposition: async ( { compositionApiResponse }, context, defaultHandler ) => { const result = await defaultHandler({ compositionApiResponse } as any); return { ...result, revalidate: 30, // Re-generate this page at most every 30 seconds }; }, });

With revalidate: 30, Next.js serves the cached page to visitors but regenerates it in the background if the cached version is older than 30 seconds. This is the "stale-while-revalidate" pattern.


Build time: Proportional to number of pages. First visit (known pages): Instant. First visit (new pages): On-demand render, then cached. Best for: Most sites with moderate page counts.

Build time: Fastest (seconds). First visit: On-demand render, then cached. Best for: Sites with many pages where build time is a concern.

To return an empty paths array:

export const getStaticPaths = () => ({ paths: [], fallback: true, });

Combines pre-rendering with periodic background regeneration:

export const getStaticProps = withUniformGetStaticProps({ param: "slug", handleComposition: async ({ compositionApiResponse }, _ctx, defaultHandler) => { const result = await defaultHandler({ compositionApiResponse } as any); return { ...result, revalidate: 60, // Refresh every 60 seconds }; }, });

Build time: Proportional to pages. Content freshness: Within the revalidation window. Best for: Content that changes regularly but does not need instant updates.

note

For most Uniform projects, Option A (SSG with fallback) combined with on-demand revalidation via webhooks provides the best balance of performance and content freshness. Time-based revalidation (revalidate) is useful as a safety net but should not be your primary cache invalidation strategy.


Use the callback option to filter which paths are pre-rendered:

export const getStaticPaths = withUniformGetStaticPaths({ callback: async (nodes) => { // Only pre-render top-level pages return nodes.filter( (node) => node.path && node.path.split("/").filter(Boolean).length <= 1 ); }, });

Use rootPath to only fetch paths under a specific project map node:

export const getStaticPaths = withUniformGetStaticPaths({ rootPath: "/blog", prefix: "/blog", });

Override the default client if you need custom API host or authentication:

import { ProjectMapClient } from "@uniformdev/project-map"; const client = new ProjectMapClient({ apiKey: process.env.UNIFORM_API_KEY, projectId: process.env.UNIFORM_PROJECT_ID, apiHost: process.env.UNIFORM_API_HOST, }); export const getStaticPaths = withUniformGetStaticPaths({ client, });

When content is published in Uniform, you want cached static pages to update without a full rebuild. Next.js Page Router supports on-demand revalidation through the res.revalidate() API.

Create a dedicated API route to handle revalidation webhooks:

// pages/api/revalidate.ts import type { NextApiRequest, NextApiResponse } from "next"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { // Validate the secret if (req.query.secret !== process.env.UNIFORM_PREVIEW_SECRET) { return res.status(401).json({ message: "Invalid token" }); } try { const { paths } = req.body; if (!paths || !Array.isArray(paths)) { // If no specific paths, revalidate the home page as a fallback await res.revalidate("/"); return res.json({ revalidated: true, paths: ["/"] }); } // Revalidate each affected path const results = await Promise.allSettled( paths.map((path: string) => res.revalidate(path)) ); return res.json({ revalidated: true, paths, results: results.map((r) => r.status), }); } catch (err) { return res.status(500).json({ message: "Error revalidating" }); } }
  1. Go to your Uniform project in the dashboard.
  2. Navigate to Settings -> Webhooks.
  3. Click Add webhook.
  4. Configure the webhook:
    • URL: https://your-site.com/api/revalidate?secret=your-secret-value-here
    • Events: Select composition.published, composition.deleted, projectMapNode.update, and any other events you want to trigger revalidation.
  5. Save the webhook.

warning

Make sure not to select *.changed webhook events (like composition.changed) as those fire during content authoring and will cause unnecessary revalidation requests. Only select *.published and *.deleted events.

For more granular revalidation, parse the webhook payload to identify which composition changed and resolve its project map paths:

// pages/api/revalidate.ts import type { NextApiRequest, NextApiResponse } from "next"; import { ProjectMapClient } from "@uniformdev/project-map"; const projectMapClient = new ProjectMapClient({ apiKey: process.env.UNIFORM_API_KEY!, projectId: process.env.UNIFORM_PROJECT_ID!, }); export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.query.secret !== process.env.UNIFORM_PREVIEW_SECRET) { return res.status(401).json({ message: "Invalid token" }); } try { const body = req.body; const compositionId = body?.payload?.compositionId; // Fetch all project map nodes to find which paths use this composition const { nodes } = await projectMapClient.getNodes({}); const affectedPaths = nodes .filter((node) => node.compositionId === compositionId) .map((node) => node.path) .filter(Boolean); if (affectedPaths.length === 0) { // Fall back to revalidating the home page await res.revalidate("/"); return res.json({ revalidated: true, paths: ["/"] }); } await Promise.allSettled( affectedPaths.map((path) => res.revalidate(path)) ); return res.json({ revalidated: true, paths: affectedPaths }); } catch (err) { return res.status(500).json({ message: "Error revalidating" }); } }

When using ISR with personalization, keep in mind:

  • Client-side personalization (default) works seamlessly with ISR. All visitors receive the same static page, and personalization variants are resolved in the browser. This may cause a brief visual flicker as the page swaps variants after hydration.

  • Edge-side personalization (via @uniformdev/context-edge-vercel) eliminates the flicker by resolving variants at the edge before the page is served. This is the recommended approach for ISR sites that need personalization without flicker. See Edge-side personalization in the main guide.

note

With client-side personalization, there is no need to generate multiple static pages per route (one for each variant combination). The single static page contains all variants, and the context engine selects the correct one on the client. This keeps build times fast.


On-demand revalidation requires your hosting provider to support the Next.js res.revalidate() API:

ProviderSupport
VercelFull support
Self-hostedSupported with persistent .next cache directory
NetlifySupported via Netlify's Next.js runtime
CloudflareSupported via Open Next adapter

Consult your provider's documentation to ensure on-demand ISR is compatible with your deployment setup.