# Incremental Static Regeneration (ISR) with Next.js Page Router

> How to configure ISR with the Uniform SDK for Next.js Page Router, including static path generation, revalidation strategies, and on-demand cache invalidation.

Source: https://docs.uniform.app/docs/sdk/nextjs-page-router/incremental-static-regeneration-isr

## Overview

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`.

---

## How ISR works with the Page Router

```mermaid
flowchart TD
  A["next build"] --> B["getStaticPaths\nFetch paths from Project Map"]
  B --> C["getStaticProps\nResolve composition for each path"]
  C --> D["Pre-render HTML + JSON"]
  D --> E["Deploy to CDN"]
  E --> F{"Visitor request"}
  F -->|"Cached page exists"| G["Serve static page\n~10-50ms TTFB"]
  F -->|"No cached page\n(fallback: true)"| H["Render on demand\nthen cache"]
  G --> I{"Revalidation needed?"}
  I -->|"Time-based"| J["Stale-while-revalidate\nRe-render in background"]
  I -->|"On-demand webhook"| K["Purge + re-render"]
```

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.

---

## Basic ISR setup

### Step 1: Use `withUniformGetStaticProps` and `withUniformGetStaticPaths`

```tsx
// 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.

### Step 2: Add time-based revalidation (optional)

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

```tsx
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.

---

## Understanding the trade-offs

### Option A: SSG with fallback (recommended starting point)

```mermaid
flowchart TD
  A["getStaticPaths returns all paths"] --> B["Build pre-renders known pages"]
  B --> C["New pages rendered on first visit"]
  C --> D["All pages cached after first render"]
```

**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.

### Option B: SSG with no pre-rendering

```mermaid
flowchart TD
  A["getStaticPaths returns empty"] --> B["Build completes in seconds"]
  B --> C["Every page rendered on first visit"]
  C --> D["Cached for subsequent visitors"]
```

**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:

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

### Option C: SSG with revalidation

Combines pre-rendering with periodic background regeneration:

```tsx
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.

---

## Customizing static paths

### Filtering paths

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

```tsx
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
    );
  },
});
```

### Scoping to a subtree

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

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

### Custom project map client

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

```tsx
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,
});
```

---

## On-demand revalidation via webhooks

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](https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation) through the `res.revalidate()` API.

### How it works

```mermaid
flowchart TD
  A["Editor publishes content"] --> B["Uniform sends webhook POST"]
  B --> C["pages/api/revalidate.ts"]
  C --> D{"Secret valid?"}
  D -->|"Yes"| E["Look up affected paths"]
  E --> F["Call res.revalidate(path)"]
  F --> G["Next.js re-renders the page"]
  G --> H["Next visitor gets fresh content"]
  D -->|"No"| I["401 Unauthorized"]
```

### Step 1: Create the revalidation API route

Create a dedicated API route to handle revalidation webhooks:

```ts
// 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" });
  }
}
```

### Step 2: Configure the webhook in Uniform

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.

### Step 3: Handle composition-specific paths

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

```ts
// 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" });
  }
}
```

---

## ISR with personalization

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](https://docs.uniform.app/docs/sdk/nextjs-page-router#edge-side-personalization-vercel) 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.

---

## CDN provider compatibility

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

| Provider | Support |
| --- | --- |
| Vercel | Full support |
| Self-hosted | Supported with persistent `.next` cache directory |
| Netlify | Supported via Netlify's Next.js runtime |
| Cloudflare | Supported via Open Next adapter |

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

---
