Incremental Static Regeneration (ISR)

Developer preview

This feature is in developer preview. Use with caution as it may change unexpectedly. For more information, contact us.

Developer Preview

This page documents the developer preview of the SDK, version 20.81.1-alpha.25.sha-f076f9b857. In this version, each published route has one cached page for all visitors. The stable SDK made one page for each combination of personalization and test variants.

The middleware rewrites each request to /uniform/[code]. The code identifies the route, not the visitor. Thus Next.js can cache one page for each route, and serve it to all visitors. Personalizations and A/B tests get their variants in the browser or at the edge. Refer to Personalization and A/B tests.

The code contains these values. A different value makes a different code, and thus a different cached page:

  • The route path, with the query strings that you list in queryStrings
  • The keys from rewriteRequestPath
  • The composition state: published, draft or editor
  • The preview mode
  • The default consent
  • The locale and the release
  • The edge mode flag

Draft and editor requests render on each request. Next.js does not cache them.

Return an empty array from generateStaticParams. The build prerenders no pages. Next.js renders each page on its first visit, and serves it from the cache after that:

app/uniform/[code]/page.tsx

// Render each page on its first visit, then serve it from the cache. export const generateStaticParams = async () => [];

This is enough for most sites. Only the first visitor of each page waits for the render.

warning

With Cache Components (cacheComponents: true), Next.js does not accept an empty generateStaticParams, and the build fails. Return at least one code. Refer to the Next.js message about empty generateStaticParams.

To make the first visit fast, prerender the most important pages at build time with createUniformStaticParams. Next.js still renders the other pages on their first visit, because dynamicParams is true by default.

app/uniform/[code]/page.tsx

import { createUniformStaticParams } from "@uniformdev/next-app-router"; export const generateStaticParams = () => createUniformStaticParams({ paths: ["/", "/about", "/contact"] });

createUniformStaticParams does these steps for each path:

  1. It applies the rewrite function, if you give one.
  2. It gets the published route from the Uniform Route API. A path that has no composition gives no code.
  3. It makes one code for each defaultConsent value and each edgeMode value.

The build time grows with the number of paths, not with the number of variants.

warning

Put the paths of the visitors in paths, for example /about. Do not put internal code paths, for example /uniform/3~64~L2Fib3V0~~3.

A prebuilt page is used only when its code is the same as the code from the middleware. Two values in the code depend on your middleware:

OptionDefaultSet it when
edgeModefalseYou use an edge middleware. With vercelUniformEdgeMiddleware, use [true, false], because the default filter skips pages that have no placements. With your own filter, give the same filter.
defaultConsentThe value of the server configurationThe middleware sets defaultConsent for each request. List each value that it can write, for example [true, false].

app/uniform/[code]/page.tsx

export const generateStaticParams = () => createUniformStaticParams({ paths: ["/", "/about"], edgeMode: [true, false], defaultConsent: [true, false], });

This example makes a maximum of 4 codes for each path.

Get the paths from the Uniform project map, so the build includes new pages without a code change:

app/uniform/[code]/page.tsx

import { createUniformStaticParams, getProjectMapClient } from "@uniformdev/next-app-router"; async function getStaticPaths(): Promise<string[]> { const { nodes } = await getProjectMapClient({ cache: { type: "default" } }).getNodes({}); // Skip nodes with dynamic segments, for example /products/:slug. return (nodes ?? []).map((node) => node.path).filter((path) => !path.includes(":")); } export const generateStaticParams = async () => createUniformStaticParams({ paths: await getStaticPaths() });

To prerender only the top-level pages, filter the paths:

const paths = (await getStaticPaths()).filter((path) => path.split("/").filter(Boolean).length <= 1);

For a full example, refer to the Component Starter Kit.

Put each locale and path in paths. For examples, refer to Localize your app.

If the middleware changes the path with rewriteRequestPath, give the same change to createUniformStaticParams. The codes are then the same as the codes from the middleware:

export const generateStaticParams = () => createUniformStaticParams({ paths: ["/", "/about", "/contact"], rewrite: async ({ path }) => ({ path: `/en${path === "/" ? "" : path}` }), });

note

keys from rewriteRequestPath go into the code. If your middleware adds keys from the request, for example from a query string, the build cannot know them. Do not prerender these paths.

createUniformPlaygroundStaticParams makes the codes for the playground route. It uses only the first path of paths, and makes one code for each defaultConsent value. Playground requests are draft requests, so most apps do not need to prerender them.


When an author publishes content, Uniform sends a webhook to your app. The POST handler in app/api/preview/route.ts revalidates the cache tags and paths of the changed content. The next request gets the old page, and starts a new render in the background. The requests after that get the new page.

Step 1: Add the POST handler#

app/api/preview/route.ts

import { createPreviewPOSTRouteHandler } from "@uniformdev/next-app-router/handler"; export const POST = createPreviewPOSTRouteHandler();

Set UNIFORM_PREVIEW_SECRET, UNIFORM_WEBHOOK_SECRET, or the two variables, in your host:

UNIFORM_PREVIEW_SECRET=your-secret-value UNIFORM_WEBHOOK_SECRET=whsec_your-svix-signing-secret

You can make a strong secret with openssl rand -base64 32.

  1. In Uniform, go to Settings > Webhooks.
  2. Add a webhook with the URL https://your-site.com/api/preview?secret=your-secret-value.
  3. Select these events:
    • composition.published and composition.deleted
    • entry.published and entry.deleted
    • projectmap.node.insert, projectmap.node.update and projectmap.node.delete
    • redirect.insert, redirect.update and redirect.delete
    • manifest.published
  4. Save the webhook.

For the tags that each event revalidates, and for the security checks, refer to Caching.

warning

The secret is a query string (?secret=…), not a header. The value in the URL must be the same as UNIFORM_PREVIEW_SECRET.

  1. Publish a change to a composition.
  2. Look for a POST request to /api/preview in the server log.
  3. Make sure that the response body is { "handled": true, "tags": [...], "paths": [...] }.
  4. Open the page two times. The second request shows the new content.

To do a test locally, use a tunnel service, for example ngrok or cloudflared. Set the webhook URL to the tunnel URL.

On-demand revalidation must have a host that supports Next.js cache revalidation:

HostSupport
VercelSupported, also revalidateTag
Self-hostedSupported with the Next.js standalone output and a persistent cache folder
NetlifySupported with the Netlify Next.js runtime
CloudflareSupported with the OpenNext adapter

Read the documentation of your host to make sure that it supports on-demand ISR.

To revalidate a page manually, revalidate its cache tag. The cached page uses the fetch tags of its route, so the tag expires the page too:

import { revalidateTag } from "next/cache"; revalidateTag("path:/about", "max");

Do not use revalidatePath with the path of the visitor. The middleware rewrites the request to /uniform/[code], and with a rewrite, revalidatePath must get the destination route. Refer to revalidatePath with rewrites. To revalidate all Uniform pages, use revalidatePath("/uniform/[code]", "page").