Next.js Page Router SDK

Looking for the latest Next.js SDK?

The Next.js App Router SDK v2 is the latest Uniform SDK for Next.js. It requires Next.js 16 and works exclusively with the App Router. If you are using the Page Router with Next.js 13--15, this Page Router SDK is the right choice and remains fully supported.

The Uniform SDK for Next.js Page Router provides a full-featured integration between Uniform's DXP and the Next.js Pages Router. It handles route resolution, component rendering, personalization, A/B testing, and visual editing -- with support for both Server-Side Rendering (SSR) and Static Site Generation (SSG) with Incremental Static Regeneration (ISR).

The Page Router integration is split across several packages, each with a focused responsibility:

PackagePurpose
@uniformdev/canvasCore composition data types, route client, and state constants
@uniformdev/canvas-nextNext.js Page Router helpers: route resolution (getServerSideProps / getStaticProps / getStaticPaths), preview handler, and UniformRichText
@uniformdev/canvas-reactReact components and hooks: UniformComposition, UniformSlot, UniformText, registerUniformComponent, and contextual editing support
@uniformdev/contextUniform Context engine: visitor scoring, personalization manifest, quirks, and plugins
@uniformdev/context-reactReact bindings for Context: UniformContext provider, useQuirks, useScores, Personalize, Test, and Track components
@uniformdev/context-nextNext.js-specific Context utilities: NextCookieTransitionDataStore for SSR cookie-based score persistence, enableNextSsr, and UniformAppProps type
@uniformdev/project-mapProject map client for fetching site tree nodes for navigation, sitemaps, and static path generation

Understanding the request lifecycle helps you reason about configuration choices and debug issues effectively.

  1. getServerSideProps resolves the route -- When a visitor requests a page (e.g., /about), the Uniform SDK helper withUniformGetServerSideProps calls the Uniform Route API, resolves the matching composition, and returns it as page props.

  2. The page component receives the composition -- The page component receives the full RootComponentInstance as props and passes it to UniformComposition.

  3. Components render via the component registry -- UniformComposition walks the composition tree and renders each component using the registered component resolver.

  4. Visual editing works out of the box -- When previewing in the Uniform dashboard, the SDK automatically enables contextual editing via Next.js preview mode.

In SSG mode, pages are pre-rendered at build time. getStaticPaths fetches all available paths from the Uniform Project Map, and getStaticProps resolves each composition. Pages can be incrementally regenerated using ISR. See Incremental Static Regeneration (ISR) for details.


  • Next.js 13+ (Pages Router)
  • Node.js 18+
  • A Uniform project with compositions set up

The fastest way to get up and running is to start from a working project rather than integrating from scratch.

Run this in your favorite terminal and select Next.js (Page Router):

npx @uniformdev/cli@latest new

This scaffolds a new project with all required content and files pre-configured.

You can also clone the starter manually from uniformdev/examples/nextjs-starter and follow the repo readme to set up manually.

Alternatively, if you prefer to add Uniform to an existing project manually, follow the steps below.

npm install @uniformdev/canvas@latest @uniformdev/canvas-next@latest @uniformdev/canvas-react@latest @uniformdev/context@latest @uniformdev/context-react@latest @uniformdev/context-next@latest @uniformdev/project-map@latest

For CLI operations (sync, manifest download), also install:

npm install -D @uniformdev/cli@latest

Create a .env.local file in your project root:

UNIFORM_API_KEY=your-api-key UNIFORM_PROJECT_ID=your-project-id UNIFORM_PREVIEW_SECRET=your-preview-secret

note

You can find these values in your Uniform project under Settings > API Keys. See the API keys guide for more information.


The Uniform SDK requires a specific set of files to wire up routing, rendering, and preview. Here is the minimal file structure:

your-nextjs-app/ ├── pages/ │ ├── _app.tsx # App wrapper with UniformContext │ ├── [[...slug]].tsx # Catch-all route with data fetching │ ├── playground.tsx # Playground route (visual editing) │ └── api/ │ └── preview.ts # Preview handler for visual editing ├── components/ │ ├── canvasComponents.ts # Component registration (side-effect imports) │ ├── Page.tsx # Example page component │ └── Hero.tsx # Example content component ├── lib/ │ └── uniform/ │ ├── uniformContext.ts # Context factory │ └── contextManifest.json # Downloaded personalization manifest ├── next.config.js # Next.js config └── uniform.config.ts # Uniform CLI config

The following sections walk through each file.

The Page Router integration does not require a custom Next.js config wrapper. A standard next.config.js works:

// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, images: { remotePatterns: [{ protocol: "https", hostname: "*" }], }, }; module.exports = nextConfig;

Create lib/uniform/uniformContext.ts. This factory creates a Context instance that drives personalization, scoring, and quirks. In the Page Router, the manifest is downloaded locally as a JSON file (unlike the App Router, which fetches it at runtime):

// lib/uniform/uniformContext.ts import { Context, ManifestV2, ContextPlugin, enableContextDevTools, enableDebugConsoleLogDrain, } from "@uniformdev/context"; import { NextCookieTransitionDataStore } from "@uniformdev/context-next"; import { NextPageContext } from "next"; import manifest from "./contextManifest.json"; export default function createUniformContext( serverContext?: NextPageContext ): Context { const plugins: ContextPlugin[] = [ enableContextDevTools(), enableDebugConsoleLogDrain("debug"), ]; return new Context({ defaultConsent: true, manifest: manifest as ManifestV2, transitionStore: new NextCookieTransitionDataStore({ serverContext, }), plugins, }); }

Key points:

  • NextCookieTransitionDataStore persists visitor scores and quirks in cookies, enabling SSR-compatible score transfer between client and server.
  • Pass serverContext when creating the context on the server (in _document.tsx with enableNextSsr) so the store can read cookies from the incoming request.
  • The contextManifest.json file is generated by the CLI and must be downloaded before builds (see Manifest management).

Create pages/_app.tsx. The UniformContext provider wraps your entire app and supplies the context engine to all components:

// pages/_app.tsx import { UniformContext } from "@uniformdev/context-react"; import { UniformAppProps } from "@uniformdev/context-next"; import createUniformContext from "@/lib/uniform/uniformContext"; // IMPORTANT: import all Canvas-managed components so they are discovered import "@/components/canvasComponents"; import "@/styles/styles.css"; const clientContext = createUniformContext(); function MyApp({ Component, pageProps, serverUniformContext, }: UniformAppProps) { return ( <UniformContext context={serverUniformContext ?? clientContext} outputType="standard" > <Component {...pageProps} /> </UniformContext> ); } export default MyApp;

note

serverUniformContext is provided when enableNextSsr() is used in _document.tsx (see Server-side personalization below). If it is not provided, the client-side context is used as the fallback.

Create pages/[[...slug]].tsx. This is the main route handler that resolves compositions from the Uniform Route API. You can use either SSR or SSG mode.

// pages/[[...slug]].tsx import PageComposition from "@/components/PageComposition"; import { withUniformGetServerSideProps } from "@uniformdev/canvas-next/route"; import { CANVAS_DRAFT_STATE, CANVAS_PUBLISHED_STATE, } from "@uniformdev/canvas"; export const getServerSideProps = withUniformGetServerSideProps({ requestOptions: { state: process.env.NODE_ENV === "development" ? CANVAS_DRAFT_STATE : CANVAS_PUBLISHED_STATE, }, }); export default PageComposition;
// 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;

note

The param option in withUniformGetStaticProps must match your dynamic route segment name. For [[...slug]].tsx, use param: "slug".

Create components/PageComposition.tsx. This component receives the composition data from getServerSideProps or getStaticProps and renders it:

// components/PageComposition.tsx import Head from "next/head"; import { RootComponentInstance } from "@uniformdev/canvas"; import { UniformComposition } from "@uniformdev/canvas-react"; export interface PageCompositionProps { data: RootComponentInstance; } export default function PageComposition({ data: composition, }: PageCompositionProps) { const { metaTitle } = composition?.parameters || {}; return ( <> <Head> <title>{metaTitle?.value as string}</title> </Head> <main> <UniformComposition data={composition} /> </main> </> ); }

UniformComposition walks the composition tree and renders each component using the registered component resolver (see Component registration below).

Create pages/api/preview.ts to handle visual editing preview requests from the Uniform dashboard:

// pages/api/preview.ts import { createPreviewHandler } from "@uniformdev/canvas-next"; const handler = createPreviewHandler({ secret: () => process.env.UNIFORM_PREVIEW_SECRET || "", playgroundPath: "/playground", }); export default handler;

The preview handler:

  • Validates the preview secret from the query string.
  • Sets Next.js preview mode cookies (enabling draft content fetching).
  • Redirects to the composition path or the playground page.
  • Handles both GET (preview entry) and POST (live composition data from the editor) requests.

Create pages/playground.tsx for visual editing and previewing individual compositions from the Uniform dashboard:

// pages/playground.tsx import { UniformPlayground } from "@uniformdev/canvas-react"; export default function Playground() { return <UniformPlayground behaviorTracking="onLoad" />; }

Create components/canvasComponents.ts. This file imports all your Canvas-managed components so they are registered with the component store:

// components/canvasComponents.ts // IMPORTANT: all Canvas-managed components must be imported here // so they are discovered by <UniformSlot /> and rendered. import "./Page"; import "./Hero";

This file must be imported in _app.tsx (as a side-effect import) so all components are registered before any composition is rendered.


Components in the Page Router SDK use the registerUniformComponent pattern. Each component registers itself with the global component store, which UniformComposition uses to resolve component types to React components at render time.

Every component receives props through ComponentProps<TProps>:

PropTypeDescription
componentComponentInstanceThe full component instance data (type, slots, parameters, _id, etc.)
...parametersTPropsFlattened parameter values spread directly onto props

In the Page Router SDK, parameter values are flattened -- the raw value of each parameter is spread directly onto the component props. This differs from the App Router SDK, which wraps parameters in ComponentParameter<T>.

// components/Hero.tsx import { registerUniformComponent, ComponentProps, UniformText, } from "@uniformdev/canvas-react"; import { UniformRichText } from "@uniformdev/canvas-next"; import { RichTextParamValue } from "@uniformdev/canvas"; type HeroProps = ComponentProps<{ title: string; description?: RichTextParamValue; }>; const Hero: React.FC<HeroProps> = () => ( <div> <UniformText parameterId="title" as="h1" className="text-4xl font-bold" placeholder="Hero title goes here" /> <UniformRichText parameterId="description" className="prose" placeholder="Hero description goes here" /> </div> ); registerUniformComponent({ type: "hero", component: Hero, }); export default Hero;

note

registerUniformComponent is a side-effect registration. The component registers itself when its module is imported. Make sure all component modules are imported in canvasComponents.ts.

Access parameter values directly from the component prop for non-visual values (URLs, boolean flags, etc.):

import { registerUniformComponent, ComponentProps, } from "@uniformdev/canvas-react"; type BannerProps = ComponentProps<{ title: string; linkUrl: string; isVisible: boolean; }>; const Banner: React.FC<BannerProps> = ({ title, linkUrl, isVisible, }) => { if (!isVisible) return null; return ( <a href={linkUrl ?? "#"}> <h2>{title ?? "Default title"}</h2> </a> ); }; registerUniformComponent({ type: "banner", component: Banner, }); export default Banner;

UniformText renders text parameters with built-in support for inline editing in the Uniform visual editor:

<UniformText parameterId="title" // Required: the parameter ID as defined in the component type as="h1" // Optional: HTML element (default: "span") className="text-xl" // Optional: CSS class placeholder="Enter title" // Optional: placeholder shown in editor when empty isMultiline={false} // Optional: enables multi-line editing (default: false) />

warning

In the Page Router SDK, UniformText uses parameterId (a string matching the parameter name in your component definition). This differs from the App Router SDK, which passes the parameter object directly.

UniformRichText renders rich text parameters with full formatting support. It is imported from @uniformdev/canvas-next (not canvas-react):

import { UniformRichText } from "@uniformdev/canvas-next"; <UniformRichText parameterId="description" // Required: the parameter ID className="prose" // Optional: CSS class placeholder="Enter text" // Optional: placeholder shown in editor when empty />

Slots define where child components can be placed within a parent component. A page component typically has slots like content, header, and footer.

// components/Page.tsx import { registerUniformComponent, UniformSlot, ComponentProps, } from "@uniformdev/canvas-react"; type PageProps = ComponentProps; const Page: React.FC<PageProps> = () => ( <div> <header> <UniformSlot name="header" /> </header> <main> <UniformSlot name="content" /> </main> <footer> <UniformSlot name="footer" /> </footer> </div> ); registerUniformComponent({ type: "page", component: Page, }); export default Page;

note

In the Page Router SDK, UniformSlot uses the name prop (a string matching the slot name in your component definition). This differs from the App Router SDK, which passes a slot object.

UniformSlot accepts a wrapperComponent prop for wrapping the list of rendered slot items:

import { UniformSlotWrapperComponentProps } from "@uniformdev/canvas-react"; const GridWrapper: React.FC<UniformSlotWrapperComponentProps> = ({ items, }) => ( <div className="grid grid-cols-3 gap-4"> {items} </div> ); <UniformSlot name="content" wrapperComponent={GridWrapper} />

Instead of using registerUniformComponent, you can pass a custom resolveRenderer function:

import { ComponentInstance } from "@uniformdev/canvas"; import { UniformComposition } from "@uniformdev/canvas-react"; import { Hero } from "./Hero"; import { Page } from "./Page"; const resolveRenderer = (component: ComponentInstance) => { switch (component.type) { case "hero": return Hero; case "page": return Page; default: return null; } }; export default function PageComposition({ data }) { return ( <UniformComposition data={data} resolveRenderer={resolveRenderer} /> ); }

When using resolveRenderer, you do not need registerUniformComponent or the canvasComponents.ts import file.


The SDK provides three main helpers for data fetching in the Page Router. These wrap the Uniform Route API and handle composition resolution, redirects, and not-found responses automatically.

Creates a getServerSideProps implementation that resolves compositions via the Uniform Route API on every request:

import { withUniformGetServerSideProps } from "@uniformdev/canvas-next/route"; export const getServerSideProps = withUniformGetServerSideProps({ requestOptions: { state: CANVAS_PUBLISHED_STATE, }, });

Creates a getStaticProps implementation that resolves compositions at build time:

import { withUniformGetStaticProps } from "@uniformdev/canvas-next/route"; export const getStaticProps = withUniformGetStaticProps({ param: "slug", requestOptions: { state: CANVAS_PUBLISHED_STATE, }, });

Creates a getStaticPaths implementation that fetches all paths from the Uniform Project Map:

import { withUniformGetStaticPaths } from "@uniformdev/canvas-next/route"; export const getStaticPaths = withUniformGetStaticPaths();

All three helpers accept these common options:

OptionTypeDescription
clientRouteClientOverride the default route client
prefixstringStrip a prefix from the resolved URL before route resolution
modifyPath(path, context) => stringTransform the path before sending it to the Route API
projectMapIdstringOverride the project map ID (defaults to UNIFORM_PROJECT_MAP_ID env var)
requestOptionsPartial<RouteGetParameters>Override route API request parameters (state, locale, etc.)
silentbooleanDisable logging of response information and timings
handleCompositionfunctionOverride handling of a composition route response
handleRedirectfunctionOverride handling of a redirect route response
handleNotFoundfunctionOverride handling of a not-found route response

Use handleComposition to add extra data (like navigation links) to the page props:

export const getServerSideProps = withUniformGetServerSideProps({ requestOptions: { state: CANVAS_PUBLISHED_STATE, }, handleComposition: async ( { compositionApiResponse }, context, defaultHandler ) => { const { composition } = compositionApiResponse || {}; const navLinks = await getNavigationLinks(context.preview); return { props: { data: composition, navLinks, }, }; }, });
OptionTypeDescription
projectMapIdstringThe project map ID to query
rootPathstringStarting path to fetch nodes from
prefixstringPrepend this string to all returned paths
previewbooleanInclude draft compositions
requestOptionsPartial<ProjectMapNodeGetRequest>Override request parameters
callback(nodes) => Promise<nodes>Modify the node list before building paths
clientProjectMapClientOverride the default project map client
redirectClientRedirectClientOverride the default redirect client

The SDK supports Next.js Internationalized Routing. Use the prependLocale path modifier to automatically prepend the current locale to paths before sending them to the Uniform Route API:

import { withUniformGetServerSideProps } from "@uniformdev/canvas-next/route"; import { prependLocale } from "@uniformdev/canvas-next/route"; export const getServerSideProps = withUniformGetServerSideProps({ modifyPath: prependLocale, });

This works with both withUniformGetServerSideProps and withUniformGetStaticProps. Configure your locales in next.config.js:

// next.config.js module.exports = { i18n: { locales: ["en", "fr", "de"], defaultLocale: "en", }, };

By default, personalization and A/B testing are evaluated client-side by the UniformContext provider. The context engine runs in the browser, evaluates visitor scores against the personalization manifest, and selects the winning variant. This works with both SSR and SSG.

No additional code is required in your components -- the Personalize and Test system components are handled automatically by UniformComposition.

For SSR pages, you can enable server-side personalization by setting up enableNextSsr in _document.tsx. This runs the context engine on the server during the initial render, so the visitor sees the correct personalized variant without a client-side flicker:

// pages/_document.tsx import Document, { DocumentContext } from "next/document"; import { enableNextSsr } from "@uniformdev/context-next"; import createUniformContext from "@/lib/uniform/uniformContext"; class MyDocument extends Document { static async getInitialProps(ctx: DocumentContext) { const serverContext = createUniformContext(ctx); enableNextSsr(ctx, serverContext); return Document.getInitialProps(ctx); } } export default MyDocument;

enableNextSsr monkey-patches renderPage to inject the server-side context into the app as serverUniformContext. The _app.tsx then uses this server context (when available) instead of the client-side one.

For SSG pages deployed on Vercel, you can use edge middleware to evaluate personalization at the edge, achieving flicker-free personalization with static pages:

// middleware.ts import { parse } from "cookie"; import { NextRequest, NextResponse } from "next/server"; import { Context, CookieTransitionDataStore, ManifestV2, UNIFORM_DEFAULT_COOKIE_NAME, } from "@uniformdev/context"; import { createUniformEdgeMiddleware } from "@uniformdev/context-edge-vercel"; import manifest from "./lib/uniform/contextManifest.json"; export const config = { matcher: [ "/(.*?trpc.*?|(?!static|.*\\..*|_next|images|img|api|favicon.ico).*)", ], }; export async function middleware(request: NextRequest) { const previewDataCookie = request.cookies.get("__next_preview_data"); // Disable middleware in preview mode if (Boolean(previewDataCookie)) { return NextResponse.next(); } const serverCookieValue = request ? parse(request.headers.get("cookie") ?? "")[UNIFORM_DEFAULT_COOKIE_NAME] : undefined; const context = new Context({ defaultConsent: true, manifest: manifest as ManifestV2, transitionStore: new CookieTransitionDataStore({ serverCookieValue, }), }); const handler = createUniformEdgeMiddleware(); const response = await handler({ context, origin: new URL(request.url), request, }); return response; }

warning

Edge-side personalization requires the @uniformdev/context-edge-vercel package and SSG mode. Enable outputType="edge" on <UniformContext /> in _app.tsx when using this approach.


The @uniformdev/context-react package provides hooks for reading and updating visitor data on the client.

Access the Uniform Context instance:

import { useUniformContext } from "@uniformdev/context-react"; export const QuirkButton = () => { const { context } = useUniformContext(); const updateQuirk = async () => { await context.update({ quirks: { country: "Canada" }, }); }; return <button onClick={updateQuirk}>Set Country to Canada</button>; };

Provides reactive access to the current visitor's quirk values:

import { useQuirks } from "@uniformdev/context-react"; export const LocationBanner = () => { const quirks = useQuirks(); return <div>Current country: {quirks?.country ?? "Unknown"}</div>; };

Provides reactive access to the current visitor's score values:

import { useScores } from "@uniformdev/context-react"; export const InterestIndicator = () => { const scores = useScores(); return <div>Tech interest score: {scores?.tech ?? 0}</div>; };

The Context constructor accepts several options for fine-tuning personalization behavior:

import { Context, ManifestV2, ContextPlugin, enableContextDevTools, enableDebugConsoleLogDrain, } from "@uniformdev/context"; import { NextCookieTransitionDataStore } from "@uniformdev/context-next"; import manifest from "./contextManifest.json"; const context = new Context({ // Whether to allow storage by default (cookies, etc.) defaultConsent: true, // The personalization manifest (downloaded from CLI) manifest: manifest as ManifestV2, // Score persistence via cookies (SSR-compatible) transitionStore: new NextCookieTransitionDataStore({ serverContext, cookieAttributes: { expires: 30 / 1440, // 30 minutes }, }), // Plugins for dev tools, logging, etc. plugins: [ enableContextDevTools(), enableDebugConsoleLogDrain("debug"), ], // Session lifespan in milliseconds (default: 30 minutes) visitLifespan: 1800000, });
OptionTypeDescription
serverContextNextPageContext | undefinedPass the Next.js page context for server-side cookie reading
cookieAttributesobjectCookie options (expires, path, domain, secure, sameSite)
cookieNamestringOverride the default cookie name (default: ufvd)
experimental_quirksEnabledbooleanEnable quirk serialization in cookies

warning

The Page Router requires downloading the manifest locally. Unlike the App Router SDK (which fetches the manifest at runtime), the Page Router SDK reads the manifest from a local JSON file. You must download the manifest before building your project.

Add these scripts to your package.json:

{ "scripts": { "dev": "run-s uniform:manifest next:dev", "build": "run-s uniform:manifest next:build", "next:dev": "next dev", "next:build": "next build", "uniform:manifest": "uniform context manifest download --output ./lib/uniform/contextManifest.json", "uniform:push": "uniform sync push", "uniform:pull": "uniform sync pull", "uniform:publish": "uniform context manifest publish" } }

The scripts explained:

  • uniform:manifest -- Downloads the personalization manifest to a local JSON file. Must run before dev and build.
  • uniform:publish -- Required after making changes to signals, enrichments, or quirks. Publishes the personalization manifest so the download command picks up the latest version.
  • uniform:push / uniform:pull -- Syncs component definitions, compositions, and other content between your local project and the Uniform cloud.

note

Use npm-run-all (or run-s / run-p) to chain scripts. Install it with npm install -D npm-run-all.


AspectSSR (getServerSideProps)SSG + ISR (getStaticProps)
When content is fetchedEvery requestBuild time + on-demand revalidation
PersonalizationServer-side (with enableNextSsr) or client-sideClient-side or edge middleware
First visit latency~200-500ms (API call on each request)~10-50ms (served from CDN)
Content freshnessAlways freshStale until revalidated
Best forAuthenticated pages, real-time data, getting started quicklyMarketing pages, high-traffic public pages

For a detailed guide on SSG with ISR, see Incremental Static Regeneration (ISR).