# Uniform SDK for Next.js Page Router

> Comprehensive guide to integrating Uniform with Next.js Page Router, including setup, component development, personalization, and rendering strategies.

Source: https://docs.uniform.app/docs/sdk/nextjs-page-router

> **Looking for the latest Next.js App Router SDK?:**
>
> The [Next.js App Router SDK v2](https://docs.uniform.app/docs/sdk/nextjs-app-router) 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, 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).

### Packages overview

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

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

## How it works

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

### SSR mode (Server-Side Rendering)

```mermaid
flowchart TD
  A["Browser requests /about"] --> B["Next.js Server\ngetServerSideProps"]
  B -->|"Resolve route"| C["Uniform Route API"]
  C -->|"Composition data"| B
  B -->|"Props with composition"| D["Page Component"]
  D -->|"UniformComposition renders tree"| E["React Components"]
  E --> F["HTML Response"]
```

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.

### SSG mode (Static Site Generation)

```mermaid
flowchart TD
  A["next build"] --> B["getStaticPaths\nFetch all paths from Project Map"]
  B --> C["getStaticProps\nFor each path, resolve composition"]
  C -->|"Composition data"| D["Pre-render HTML"]
  D --> E["Static HTML + JSON files"]
  E --> F["CDN serves static pages"]
```

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)](./nextjs-page-router/incremental-static-regeneration-isr) for details.

---

## Getting started

### Prerequisites

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

### Quick start options

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

```bash
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](https://github.com/uniformdev/examples/tree/main/examples/nextjs-starter) and follow the repo readme to set up manually.

> **Use an AI coding assistant:**
>
> The [`uniform-nextjs-page-router`](https://github.com/uniformdev/agent-skills/tree/main/skills/uniform-nextjs-page-router) agent skill teaches coding assistants such as Claude Code, Codex, Cursor, and Copilot how to add Uniform to a Next.js Page Router project: component registration, fetching compositions, contextual editing preview, personalization, and A/B testing. To install it, see [Uniform agent skills](https://docs.uniform.app/docs/guides/ai/agent-skills).

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

### Install packages

```bash
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:

```bash
npm install -D @uniformdev/cli@latest
```

### Environment variables

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

```bash
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 **Security > Service Accounts**. See the [API access guide](https://docs.uniform.app/docs/guides/api-access) for more information.

---

## Project setup

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.

### Step 1: Next.js configuration

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

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

module.exports = nextConfig;
```

### Step 2: Uniform Context factory

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

```ts
// 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](#manifest-management)).

### Step 3: App wrapper (`_app.tsx`)

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

```tsx
// 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](#server-side-personalization) below). If it is not provided, the client-side context is used as the fallback.

### Step 4: Catch-all page route

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.

#### SSR mode (recommended for getting started)

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

#### SSG mode

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

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

### Step 5: Page composition component

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

```tsx
// 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](#component-registration) below).

### Step 6: Preview handler

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

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

### Step 7: Playground route

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

```tsx
// pages/playground.tsx
import { UniformPlayground } from "@uniformdev/canvas-react";

export default function Playground() {
  return <UniformPlayground behaviorTracking="onLoad" />;
}
```

### Step 8: Component registration

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

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

---

## Building components

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.

### Component props

Every component receives props through `ComponentProps<TProps>`:

| Prop | Type | Description |
| --- | --- | --- |
| `component` | `ComponentInstance` | The full component instance data (type, slots, parameters, _id, etc.) |
| `...parameters` | `TProps` | Flattened 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>`.

### Basic component with parameters

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

### Accessing raw parameter values

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

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

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

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

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

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

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

### Rendering slots with UniformSlot

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

### Custom slot wrapper

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

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

### Custom component resolver

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

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

---

## Data fetching helpers

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.

### withUniformGetServerSideProps

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

```tsx
import { withUniformGetServerSideProps } from "@uniformdev/canvas-next/route";

export const getServerSideProps = withUniformGetServerSideProps({
  requestOptions: {
    state: CANVAS_PUBLISHED_STATE,
  },
});
```

### withUniformGetStaticProps

Creates a `getStaticProps` implementation that resolves compositions at build time:

```tsx
import { withUniformGetStaticProps } from "@uniformdev/canvas-next/route";

export const getStaticProps = withUniformGetStaticProps({
  param: "slug",
  requestOptions: {
    state: CANVAS_PUBLISHED_STATE,
  },
});
```

### withUniformGetStaticPaths

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

```tsx
import { withUniformGetStaticPaths } from "@uniformdev/canvas-next/route";

export const getStaticPaths = withUniformGetStaticPaths();
```

### Handler options

All three helpers accept these common options:

| Option | Type | Description |
| --- | --- | --- |
| `client` | `RouteClient` | Override the default route client |
| `prefix` | `string` | Strip a prefix from the resolved URL before route resolution |
| `modifyPath` | `(path, context) => string` | Transform the path before sending it to the Route API |
| `projectMapId` | `string` | Override the project map ID (defaults to `UNIFORM_PROJECT_MAP_ID` env var) |
| `requestOptions` | `Partial<RouteGetParameters>` | Override route API request parameters (state, locale, etc.) |
| `silent` | `boolean` | Disable logging of response information and timings |
| `handleComposition` | `function` | Override handling of a composition route response |
| `handleRedirect` | `function` | Override handling of a redirect route response |
| `handleNotFound` | `function` | Override handling of a not-found route response |

### Custom composition handling

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

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

### withUniformGetStaticPaths options

| Option | Type | Description |
| --- | --- | --- |
| `projectMapId` | `string` | The project map ID to query |
| `rootPath` | `string` | Starting path to fetch nodes from |
| `prefix` | `string` | Prepend this string to all returned paths |
| `preview` | `boolean` | Include draft compositions |
| `requestOptions` | `Partial<ProjectMapNodeGetRequest>` | Override request parameters |
| `callback` | `(nodes) => Promise<nodes>` | Modify the node list before building paths |
| `client` | `ProjectMapClient` | Override the default project map client |
| `redirectClient` | `RedirectClient` | Override the default redirect client |

---

## Localization

The SDK supports Next.js [Internationalized Routing](https://nextjs.org/docs/pages/building-your-application/routing/internationalization). Use the `prependLocale` path modifier to automatically prepend the current locale to paths before sending them to the Uniform Route API:

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

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

---

## Personalization and testing

### Client-side personalization (default)

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

### Server-side personalization

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:

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

### Edge-side personalization (Vercel)

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

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

---

## Client-side context hooks

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

### useUniformContext

Access the Uniform Context instance:

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

### useQuirks

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

```tsx
import { useQuirks } from "@uniformdev/context-react";

export const LocationBanner = () => {
  const quirks = useQuirks();
  return <div>Current country: {quirks?.country ?? "Unknown"}</div>;
};
```

### useScores

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

```tsx
import { useScores } from "@uniformdev/context-react";

export const InterestIndicator = () => {
  const scores = useScores();
  return <div>Tech interest score: {scores?.tech ?? 0}</div>;
};
```

---

## Context configuration

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

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

### NextCookieTransitionDataStore options

| Option | Type | Description |
| --- | --- | --- |
| `serverContext` | `NextPageContext \| undefined` | Pass the Next.js page context for server-side cookie reading |
| `cookieAttributes` | `object` | Cookie options (`expires`, `path`, `domain`, `secure`, `sameSite`) |
| `cookieName` | `string` | Override the default cookie name (default: `ufvd`) |
| `experimental_quirksEnabled` | `boolean` | Enable quirk serialization in cookies |

---

## Manifest management

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

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

---

## Rendering strategy comparison

| Aspect | SSR (`getServerSideProps`) | SSG + ISR (`getStaticProps`) |
| --- | --- | --- |
| **When content is fetched** | Every request | Build time + on-demand revalidation |
| **Personalization** | Server-side (with `enableNextSsr`) or client-side | Client-side or edge middleware |
| **First visit latency** | ~200-500ms (API call on each request) | ~10-50ms (served from CDN) |
| **Content freshness** | Always fresh | Stale until revalidated |
| **Best for** | Authenticated pages, real-time data, getting started quickly | Marketing pages, high-traffic public pages |

For a detailed guide on SSG with ISR, see [Incremental Static Regeneration (ISR)](./nextjs-page-router/incremental-static-regeneration-isr).

---
