# Uniform SDK for Next.js App Router

> Comprehensive guide to integrating Uniform with Next.js App Router, including setup, component development, personalization, and advanced patterns.

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

> **Version 2 of the Next.js App Router SDK:**
>
> This is **v2** of the Uniform SDK for Next.js App Router. It requires **Next.js 16** and works **only with the App Router**.
>
> - **Using Next.js with the Page Router?** You can continue using the [Next.js Page Router SDK](https://docs.uniform.app/docs/sdk/nextjs-page-router), which remains fully supported.
> - **Using Next.js 15 with the App Router (earlier SDK)?** You can upgrade to this new SDK. See the [upgrade guide](https://docs.uniform.app/docs/sdk/nextjs-app-router/upgrade-from-v1) for step-by-step instructions.
> - **Planning to migrate from Page Router to App Router?** If you are upgrading your Next.js project from Page Router to App Router, you can also adopt this new SDK. [Contact support](https://uniform.dev/support) for help with this transition.

The Uniform SDK for Next.js App Router (`@uniformdev/next-app-router`) is purpose-built for **Next.js 16** and provides a first-class integration between Uniform's DXP and the App Router. It handles route resolution, component rendering, personalization, A/B testing, and visual editing -- all optimized for React Server Components and edge middleware.

### Why Next.js 16

This SDK takes full advantage of Next.js 16 capabilities, most notably the `cacheComponents` support (learn more [here](#enabling-cache-components) on how to enable it). When enabled, route resolution results are cached at the framework level, which delivers:

- **Static-like performance** -- Cached compositions are served instantly without round-trips to the Uniform API on subsequent requests, achieving response times comparable to fully static sites.
- **Flicker-free personalization** -- Personalization and A/B test variants are resolved at the edge in middleware _before_ the page renders. The visitor always receives the correct variant on the first paint -- no client-side swap, no layout shift, no flicker.
- **Edge-evaluated A/B testing** -- Tests are evaluated in middleware and the winning variant is baked into the serialized page state. The page component simply renders the result, with no client-side JavaScript required for test selection.
- **Automatic cache invalidation** -- The SDK integrates with Next.js cache tags so that publishing changes in Uniform automatically invalidates only the affected cached pages.

See [Enabling `cacheComponents`](#enabling-cache-components) below for setup instructions.

## How it works

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

```mermaid
flowchart TD
  A["Browser request /about"] --> B["Edge Middleware\nmiddleware.ts"]
  B -->|"Resolve route"| C["Uniform Route API"]
  C -->|"Composition + personalization data"| B
  B -->|"Rewrite to /uniform/[code]"| D["Page Component\napp/uniform/[code]/page.tsx"]
  D -->|"Decode page state, fetch composition"| E["UniformComposition"]
  E -->|"resolveComponent"| F["React Server Components"]
  F --> G["HTML Response"]
```

1. **Middleware intercepts the request** -- When a visitor requests a page (e.g., `/about`), the Uniform middleware resolves the route through the Uniform Route API, evaluates personalization and A/B tests at the edge, and rewrites the request to `/uniform/[code]` where `code` is a serialized page state.
2. **The page component receives the code** -- The `app/uniform/[code]/page.tsx` route handler receives the encoded page state, resolves the full composition data, and renders the component tree.
3. **Components render server-side** -- All Uniform components are React Server Components by default. Client interactivity (quirks, scores, context updates) is added only where needed.
4. **Visual editing works out of the box** -- When previewing in the Uniform dashboard, the SDK automatically enables inline editing, contextual editing attributes, and live preview capabilities.

---

## Getting started

### Prerequisites

- Next.js 16+ (App Router)
- Node.js 22+
- 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 thisin your favorite terminal and select **Next.js**:

```bash
npx @uniformdev/cli@latest new
```

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

You will have two options:

1. **Component Starter Kit** -- A feature-rich starter with pre-built components, design tokens, TailwindCSS theming, and more.

   > You can also clone it manually from [uniformdev/component-starter-kit-next-approuter](https://github.com/uniformdev/component-starter-kit-next-approuter) and follow the repo readme to setup manually.
2. **Hello World** -- A minimal, fully functional reference app showing the essential Uniform + App Router integration.

   > You can also clone it from [uniformdev/examples/nextjs-app-router-v2](https://github.com/uniformdev/examples/tree/main/examples/nextjs-app-router-v2).

> **Use an AI coding assistant:**
>
> The [`uniform-nextjs-app-router`](https://github.com/uniformdev/agent-skills/tree/main/skills/uniform-nextjs-app-router) agent skill teaches coding assistants such as Claude Code, Codex, Cursor, and Copilot how to add Uniform to a Next.js App Router project: middleware, the composition route, component mapping, preview, personalization, and caching. 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/next-app-router@latest
```

This is the only required package. It includes the server-side SDK, component utilities, middleware, config helpers, and preview handlers.

For advanced client-side context customization, you may also need:

```bash
npm install @uniformdev/next-app-router-client@latest @uniformdev/context@latest
```

### Environment variables

Create a `.env` 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/
├── app/
│   ├── api/
│   │   └── preview/
│   │       └── route.ts           # Preview handler for visual editing
│   ├── layout.tsx                 # Root layout (standard Next.js)
│   ├── uniform/
│   │   └── [code]/
│   │       └── page.tsx           # Main composition route
│   └── playground/
│       └── [code]/
│           └── page.tsx           # Playground route (visual editing)
├── components/
│   └── resolveComponent.ts        # Maps Uniform types to React components
├── middleware.ts                   # Edge middleware (required)
├── uniform.server.config.ts       # Server configuration (optional)
└── next.config.ts                 # Next.js config with withUniformConfig
```

The following sections walk through each file.

### Step 1: Next.js configuration

Wrap your Next.js configuration with `withUniformConfig`. This sets up the necessary aliases for the Uniform server config:

```ts
// next.config.ts
import { withUniformConfig } from "@uniformdev/next-app-router/config";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // your existing Next.js config options
};

export default withUniformConfig(nextConfig);
```

`withUniformConfig` automatically detects whether a `uniform.server.config.ts` file exists in your project root and sets up Webpack/Turbopack aliases accordingly. If no config file is found, default settings are used.

### Step 2: Middleware

Create `middleware.ts` at the root of your project. The middleware is **required** -- it intercepts incoming requests, resolves routes through the Uniform Route API, evaluates personalization rules at the edge, and rewrites requests to the composition page handler.

> **Warning:**
>
> This guide is for customers who are **already using the App Router** with the earlier Uniform SDK packages (`@uniformdev/canvas-next-rsc`, `@uniformdev/canvas-react`, etc.) and want to upgrade to the v2 SDK. If you are migrating from the **Page Router** to the App Router, [contact support](https://uniform.dev/support) for assistance.
>
> **Next.js 16 users:** To support Uniform preview for apps deployed to Vercel, the middleware file **must** be named `middleware.ts` and you should add `runtime: "experimental-edge"` to your config export:

```ts
// middleware.ts
import { uniformMiddleware } from "@uniformdev/next-app-router/middleware";

export default uniformMiddleware();

export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
  runtime: "experimental-edge",
};
```

### Step 3: Main composition route

Create `app/uniform/[code]/page.tsx`. This is the route that the middleware rewrites requests to after resolving the composition:

```tsx
// app/uniform/[code]/page.tsx
import {
  resolveRouteFromCode,
  UniformComposition,
  UniformPageParameters,
} from "@uniformdev/next-app-router";
import { resolveComponent } from "@/components/resolveComponent";

export default async function UniformPage(props: UniformPageParameters) {
  const { code } = await props.params;
  return (
    <UniformComposition
      code={code}
      resolveRoute={resolveRouteFromCode}
      resolveComponent={resolveComponent}
    />
  );
}
```

Key points about `UniformComposition`:

- It is an async server component that fetches and renders the full composition tree.
- It internally handles `UniformContext` setup (wrapped in `Suspense`), so you do **not** place `UniformContext` in `layout.tsx`.
- If the route cannot be resolved, it automatically calls `notFound()`.
- You can optionally add `generateStaticParams` for build-time pre-rendering -- see [Static generation (ISR)](./incremental-static-regeneration-isr) below.

### Step 4: Playground route

Create `app/playground/[code]/page.tsx`. The playground route enables visual editing and previewing individual compositions from the Uniform dashboard:

```tsx
// app/playground/[code]/page.tsx
import {
  PlaygroundParameters,
  resolvePlaygroundRoute,
  UniformPlayground,
} from "@uniformdev/next-app-router";
import { resolveComponent } from "@/components/resolveComponent";

export default async function PlaygroundPage({ params }: PlaygroundParameters) {
  const { code } = await params;
  return (
    <UniformPlayground
      code={code}
      resolveRoute={resolvePlaygroundRoute}
      resolveComponent={resolveComponent}
    />
  );
}
```

### Step 5: Preview handler and ISR handler

Create `app/api/preview/route.ts` to handle both preview requests (`GET` handler) and ISR requests (`POST` handler) from Uniform:

```ts
// app/api/preview/route.ts
import {
  createPreviewGETRouteHandler,
  createPreviewPOSTRouteHandler,
  createPreviewOPTIONSRouteHandler,
} from "@uniformdev/next-app-router/handler";

export const GET = createPreviewGETRouteHandler({
  resolveFullPath: ({ path }) => (path ? path : "/playground"),
});
export const POST = createPreviewPOSTRouteHandler();
export const OPTIONS = createPreviewOPTIONSRouteHandler();
```

### Step 6: Component resolution

Create `components/resolveComponent.ts`. This function maps Uniform component types (defined in your Uniform project) to React components:

```ts
// components/resolveComponent.ts
import {
  ResolveComponentFunction,
  type ResolveComponentResult,
} from "@uniformdev/next-app-router";

import { PageComponent } from "./page";
import { HeroComponent } from "./hero";

export const resolveComponent: ResolveComponentFunction = ({ component }) => {
  let result: ResolveComponentResult | undefined;

  if (component.type === "page") {
    result = { component: PageComponent };
  } else if (component.type === "hero") {
    result = { component: HeroComponent };
  }

  return result || {
    component: ({ type }) => <div>Component not found: {type}</div>,
  };
};
```

The `resolveComponent` function receives the raw `ComponentInstance` from the Uniform canvas data and returns a `ResolveComponentResult` containing the React component to render. It is called for every component in the composition tree.

---

## Building components

All Uniform components receive standardized props through the `ComponentProps` type. Components are React Server Components by default -- only add `"use client"` when you need browser APIs or interactivity.

### Component props

Every component receives these props via `ComponentProps<TParameters, TSlotNames>`:

| Prop | Type | Description |
| --- | --- | --- |
| `type` | `string` | The component type identifier (e.g., `"hero"`) |
| `variant` | `string \| undefined` | Active variant ID when inside a personalization or test |
| `parameters` | `TParameters` | The component's parameter values, each wrapped in `ComponentParameter<T>` |
| `slots` | `Record<TSlotNames, SlotDefinition>` | Child component slots |
| `component` | `ComponentContext` | Component metadata (`_id`, `_parentId`, `slotName`, `slotIndex`) |
| `context` | `CompositionContext` | Composition-level metadata (`_id`, `type`, `state`, `isContextualEditing`, `matchedRoute`, `dynamicInputs`) |

### Basic component with parameters

Parameters in Uniform are typed values managed through the Uniform dashboard. In your components, each parameter must be typed with `ComponentParameter<T>`:

```tsx
// components/hero.tsx
import {
  ComponentParameter,
  ComponentProps,
  UniformText,
  UniformRichText,
} from "@uniformdev/next-app-router/component";

// Define parameter types -- always mark as optional
type HeroProps = {
  title?: ComponentParameter<string>;
  description?: ComponentParameter<string>;
};

export const HeroComponent = ({
  parameters: { title, description },
  component,
}: ComponentProps<HeroProps>) => {
  return (
    <section>
      <UniformText
        component={component}
        parameter={title}
        as="h1"
        className="text-4xl font-bold"
        placeholder="Enter title here"
      />
      <UniformRichText
        component={component}
        parameter={description}
        placeholder="Enter description here"
      />
    </section>
  );
};
```

> **Warning:**
>
> **Always mark parameters as optional** using `?`. Even if a parameter is marked as required in the Uniform component definition, it can be `undefined` at runtime (e.g., when a new component is first added to a composition and hasn't been filled in yet).

### Accessing raw parameter values

When you need to read a parameter value directly (for example, for non-visible values like image alt text, URLs, or conditional logic), access the `.value` property:

```tsx
import { ComponentParameter, ComponentProps } from "@uniformdev/next-app-router/component";

type BannerProps = {
  title?: ComponentParameter<string>;
  linkUrl?: ComponentParameter<string>;
  isVisible?: ComponentParameter<boolean>;
};

export const BannerComponent = ({
  parameters: { title, linkUrl, isVisible },
}: ComponentProps<BannerProps>) => {
  if (isVisible?.value === false) return null;

  return (
    <a href={linkUrl?.value ?? "#"}>
      <h2>{title?.value ?? "Default title"}</h2>
    </a>
  );
};
```

### UniformText

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

```tsx
<UniformText
  component={component}      // Required: ComponentContext from props
  parameter={title}           // Required: the ComponentParameter<string>
  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)
  render={(value) => value?.toUpperCase()} // Optional: transform the value
/>
```

`UniformText` supports conditional parameter values driven by quirks. It automatically evaluates quirk-based conditions and renders the matching value.

### UniformRichText

`UniformRichText` renders rich text parameters with full formatting support:

```tsx
<UniformRichText
  component={component}      // Required: ComponentContext from props
  parameter={description}    // Required: the ComponentParameter<ParameterRichTextValue>
  as="div"                   // Optional: wrapper element (default: "div"), set to null for no wrapper
  className="prose"          // Optional: CSS class
  placeholder="Enter text"   // Optional: placeholder shown in editor when empty
  resolveRichTextRenderer={customResolver} // Optional: custom node renderers
/>
```

The default link renderer automatically emits any [custom link attributes](https://docs.uniform.app/docs/guides/models/components/parameters#link) and anchors set by the editor. If you override the `link` node renderer, use the `linkParamValueToAnchorProps` helper from `@uniformdev/canvas-react` to keep this behavior (see [rich text parameter utilities](https://docs.uniform.app/docs/sdk/parameter-utilities/rich-text-parameters#custom-rich-text-node-renderers)).

---

## Slots

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

### Rendering slots with UniformSlot

```tsx
// components/page.tsx
import { ComponentProps, UniformSlot } from "@uniformdev/next-app-router/component";

type PageProps = unknown;
type PageSlots = "content" | "header" | "footer";

export const PageComponent = ({ slots }: ComponentProps<PageProps, PageSlots>) => {
  return (
    <>
      <header>
        <UniformSlot slot={slots.header} />
      </header>
      <main>
        <UniformSlot slot={slots.content} />
      </main>
      <footer>
        <UniformSlot slot={slots.footer} />
      </footer>
    </>
  );
};
```

### Custom slot rendering

`UniformSlot` accepts a `children` render function for wrapping individual slot items:

```tsx
<UniformSlot slot={slots.content}>
  {({ child, _id, key, slotName, slotIndex }) => (
    <div key={key} data-id={_id} className="my-4 border-b pb-4">
      {child}
    </div>
  )}
</UniformSlot>
```

### Rendering slot items directly

You can iterate over slot items directly for full control over rendering:

```tsx
export const PageComponent = ({ slots }: ComponentProps<PageProps, PageSlots>) => {
  return (
    <ul>
      {slots.content.items.map((item) => (
        <li key={item?._id}>{item?.component}</li>
      ))}
    </ul>
  );
};
```

### getUniformSlot utility

The `getUniformSlot` function extracts slot items as an array of `ReactNode`:

```tsx
import { getUniformSlot } from "@uniformdev/next-app-router/component";

const items = getUniformSlot({ slot: slots.content });
// Returns ReactNode[] | undefined
```

This is useful when you need to count items, conditionally render, or apply array operations before rendering.

---

## Server configuration

The optional `uniform.server.config.ts` file (placed in your project root) lets you configure caching, consent defaults, and experimental features:

```ts
// uniform.server.config.ts
import { UniformServerConfig } from "@uniformdev/next-app-router/config";

const config: UniformServerConfig = {
  // Default storage consent for new visitors (default: true)
  defaultConsent: true,

  // Path to the playground page handler (default: "/uniform/playground")
  playgroundPath: "/uniform/playground",

  // Context options
  context: {
    // Disable Uniform Context dev tools in production (default: false)
    disableDevTools: false,
  },

  // Enable quirk serialization (default: true)
  quirkSerialization: true,

  // Enable runtime cache in middleware (default: true)
  middlewareRuntimeCache: true,

  // Experimental features
  experimental: {
    // Enable Vercel Visual Editing support (default: false)
    vercelVisualEditing: false,
    // Retain old route data while fetching new (requires middlewareRuntimeCache)
    disableSwrMiddlewareCache: false,
  },
};

export default config;
```

If this file does not exist, the SDK uses sensible defaults. The `withUniformConfig` wrapper in `next.config.ts` automatically picks up this file.

---

## Middleware configuration

The middleware is the most critical part of the Uniform integration. It runs at the edge and handles route resolution, personalization evaluation, and request rewriting.

### Basic middleware

The simplest setup uses `uniformMiddleware()` with no options:

```ts
import { uniformMiddleware } from "@uniformdev/next-app-router/middleware";

export default uniformMiddleware();
```

### Using handleUniformRoute directly

For more control, use `handleUniformRoute` directly:

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { NextRequest } from "next/server";

export default (request: NextRequest) => {
  return handleUniformRoute({ request });
};
```

### Middleware options

| Option | Type | Description |
| --- | --- | --- |
| `rewriteRequestPath` | `(options) => Promise<RewriteRequestPathResult>` | Transform the incoming request path before route resolution |
| `rewriteDestinationPath` | `(options) => Promise<string>` | Transform the output path after resolution |
| `pathPatternsWithVariations` | `string[]` | Paths that should pre-compute personalizations |
| `release` | `{ id: string }` | Content release ID to resolve against |
| `quirks` | `Quirks` | Custom quirks to inject into the visitor context |
| `defaultConsent` | `boolean` | Override the default consent setting |
| `locale` | `string` | Locale to use for route resolution |

### Setting quirks in middleware

Inject custom quirks based on request data:

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { NextRequest } from "next/server";

export default (request: NextRequest) => {
  return handleUniformRoute({
    request,
    quirks: {
      browser: request.headers.get("user-agent")?.includes("Chrome")
        ? "chrome"
        : "other",
    },
  });
};
```

### Locale handling in middleware

For localized sites, use `rewriteRequestPath` to prepend locale information:

```ts
import { uniformMiddleware } from "@uniformdev/next-app-router/middleware";

const locales = ["en", "fr", "de"];
const defaultLocale = "en";

export default uniformMiddleware({
  rewriteRequestPath: async ({ url }) => {
    const [firstSegment] = url.pathname.split("/").filter(Boolean);
    const hasLocale = firstSegment && locales.includes(firstSegment);

    return {
      path: hasLocale ? url.pathname : `/${defaultLocale}${url.pathname}`,
    };
  },
});
```

### Specifying locale directly

If you determine the locale from cookies or headers:

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { NextRequest } from "next/server";

export default (request: NextRequest) => {
  const locale = request.cookies.get("CUSTOM_LOCALE")?.value || "en";
  return handleUniformRoute({
    request,
    locale,
  });
};
```

### Custom route mapping

Use `findRouteMatch` to map dynamic URL patterns to Uniform project map nodes:

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { findRouteMatch, type CustomRoute } from "@uniformdev/next-app-router";
import { NextRequest } from "next/server";

const customRoutes: CustomRoute[] = [
  { id: "news-listing", pattern: "/news/:category" },
  { id: "product-detail", pattern: "/products/:slug" },
];

export default (request: NextRequest) => {
  return handleUniformRoute({
    request,
    rewriteRequestPath: async ({ url }) => {
      const routeMatch = findRouteMatch(customRoutes, url.pathname);

      if (routeMatch?.route.id === "news-listing") {
        return {
          path: "/news-listing",
          keys: { category: routeMatch.params.category },
        };
      }

      if (routeMatch?.route.id === "product-detail") {
        return {
          path: "/product-detail",
          keys: { slug: routeMatch.params.slug },
        };
      }
    },
  });
};
```

The `keys` object passes dynamic URL segments as dynamic inputs to the composition. These are accessible in the component via `context.dynamicInputs`.

### Adding URL query string values to page state

Query string values, and other custom values, can be read in middleware and passed through the `keys` object to make them available server-side while rendering the page. These values will be accessible in your component via `context.pageState.keys`. It is recommended to only include keys that are needed to render the page.

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { findRouteMatch, type CustomRoute } from "@uniformdev/next-app-router";
import { NextRequest } from "next/server";

const NEWS_LISTING_ID = "news-listing";

const customRoutes: CustomRoute[] = [
  {
    id: NEWS_LISTING_ID,
    pattern: "/news/:category",
  },
];

export default (request: NextRequest) => {
  return handleUniformRoute({
    request,
    rewriteRequestPath: async ({ url }) => {
      const routeMatch = findRouteMatch(customRoutes, url.pathname);

      if (routeMatch?.route.id === NEWS_LISTING_ID) {
        return {
          path: "/news-listing",
          keys: {
            categoryId: url.searchParams.get("categoryId") ?? "",
          },
        };
      }
    },
  });
};
```

### Moving the uniform page to a different location

If you want to place the uniform catch-all route at a different path (e.g., under a locale segment):

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { NextRequest } from "next/server";

export default (request: NextRequest) => {
  const locale = "en";
  return handleUniformRoute({
    request,
    rewriteDestinationPath: async (options) => {
      if (options.source === "route") {
        return `/${locale}/uniform/${options.code}`;
      }
      return `/${locale}/playground/${options.code}`;
    },
  });
};
```

You would then place your page at `app/[locale]/uniform/[code]/page.tsx`.

### Scoping middleware to specific paths

Use Next.js middleware `config.matcher` to limit which paths Uniform handles:

```ts
export const config = {
  matcher: ["/", "/about", "/products/:path*"],
};
```

### Override default consent per request

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { NextRequest } from "next/server";

export default (request: NextRequest) => {
  const hasConsent = request.cookies.get("cookie-consent")?.value === "true";
  return handleUniformRoute({
    request,
    defaultConsent: hasConsent,
  });
};
```

### Content release support

Switch to a specific content release by passing the release ID through middleware:

```ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { NextRequest } from "next/server";

export default (request: NextRequest) => {
  const releaseId = request.nextUrl.searchParams.get("release");

  return handleUniformRoute({
    request,
    release: releaseId ? { id: releaseId } : undefined,
  });
};
```

---

## Personalization and testing

Personalization and A/B testing are configured in the Uniform dashboard and evaluated automatically at the edge by the middleware. No additional code is required in your components -- the SDK renders the winning variant transparently.

### Server-side precomputation

If you want to evaluate tests and personalizations server-side (for fully static output), use `precomputeComposition`:

```tsx
import {
  resolveRouteFromCode,
  UniformComposition,
  UniformPageParameters,
  precomputeComposition,
} from "@uniformdev/next-app-router";

export default async function UniformPage(props: UniformPageParameters) {
  const { code } = await props.params;
  // precomputeComposition evaluates and replaces personalization/test
  // containers with the winning variants before rendering
  return (
    <UniformComposition
      code={code}
      resolveRoute={resolveRouteFromCode}
      resolveComponent={resolveComponent}
    />
  );
}
```

`precomputeComposition` walks the composition tree and replaces personalization and test containers with their resolved variants. You can selectively control which tests and personalizations to evaluate:

```tsx
await precomputeComposition({
  pageState: result.pageState,
  route: result.route,
  evaluateTests: true,              // or a filter function
  evaluatePersonalizations: (pz) => pz.name !== "skip-this-one",
});
```

### Vercel geo-IP quirks

When deployed on Vercel, the middleware automatically populates quirks from Vercel's geo-IP headers:

| Vercel Header | Quirk Key |
| --- | --- |
| `x-vercel-ip-country` | `vc-country` |
| `x-vercel-ip-country-region` | `vc-region` |
| `x-vercel-ip-city` | `vc-city` |

These quirks are available for personalization rules without any additional configuration.

---

## Client-side context

Most Uniform components are server-rendered and don't need client-side JavaScript. However, certain features require client-side interactivity:

- Reading or updating visitor quirks
- Tracking visitor scores
- Responding to context changes in real-time

> **Warning:**
>
> Any component that uses client-side hooks (`useUniformContext`, `useQuirks`, `useScores`) or browser APIs must be marked with `"use client"` at the top of the file. This is a React Server Components requirement, not specific to Uniform.
>
> **Push client components as far down the component tree as possible.** Adding `"use client"` to a component turns it and all of its children into client components, which means more JavaScript shipped to the browser and no server-side rendering for that subtree. Instead of making an entire page component a client component, extract the interactive piece (e.g., a button that updates a quirk) into its own small client component and embed it within the server-rendered parent.

### useUniformContext

Access the Uniform Context instance on the client:

```tsx
"use client";

import { useUniformContext } from "@uniformdev/next-app-router/component";
import { useEffect, useState } from "react";

export const QuirkButton = () => {
  const { context } = useUniformContext();
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (context?.quirks !== undefined) {
      setIsLoading(false);
    }
  }, [context?.quirks]);

  const updateQuirk = async () => {
    setIsLoading(true);
    try {
      await context?.update({
        quirks: { country: "Canada" },
      });
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <button onClick={updateQuirk} disabled={isLoading}>
      Set Country to Canada
    </button>
  );
};
```

> **Note:**
>
> The `context` object may be `undefined` initially while the client-side context initializes. Always check for `undefined` before using it.

### useQuirks

Provides reactive access to the current visitor's quirk values. The component re-renders whenever quirks change:

```tsx
"use client";

import { useQuirks } from "@uniformdev/next-app-router/component";

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
"use client";

import { useScores } from "@uniformdev/next-app-router/component";

export const InterestIndicator = () => {
  const scores = useScores();

  return <div>Tech interest score: {scores?.tech ?? 0}</div>;
};
```

---

## Custom client context

For advanced scenarios like custom Context plugins, analytics integrations, or custom dev tools behavior, create a custom client context component:

```tsx
// components/CustomUniformClientContext.tsx
"use client";

import { ContextPlugin, enableContextDevTools } from "@uniformdev/context";
import { useRouter } from "next/navigation";
import {
  createClientUniformContext,
  useInitUniformContext,
  ClientContextComponent,
} from "@uniformdev/next-app-router-client";

export const CustomUniformClientContext: ClientContextComponent = ({
  manifest,
  disableDevTools,
  defaultConsent,
  experimentalQuirkSerialization,
  compositionMetadata,
}) => {
  const router = useRouter();

  useInitUniformContext(() => {
    const plugins: ContextPlugin[] = [];

    if (!disableDevTools) {
      plugins.push(
        enableContextDevTools({
          onAfterMessageReceived: () => {
            router.refresh();
          },
        })
      );
    }

    return createClientUniformContext({
      manifest,
      plugins,
      defaultConsent,
      experimental_quirksEnabled: experimentalQuirkSerialization,
    });
  }, compositionMetadata);

  return null;
};
```

Pass it to `UniformComposition`:

```tsx
<UniformComposition
  code={code}
  resolveRoute={resolveRouteFromCode}
  resolveComponent={resolveComponent}
  clientContextComponent={CustomUniformClientContext}
/>
```

---

## Enhancing composition data

To enhance composition data (addjust content or structure, fetch additional data, etc.), you need to create a custom data client and configure it in two places: the middleware handler and the `UniformComposition` component.

First, create a custom data client. It's recommended to put this in a central location (e.g., `lib/dataClient.ts`) as you'll need to use the same instance in both places:

```ts
import {
  DefaultDataClient,
  EnhanceRouteOptions,
} from "@uniformdev/next-app-router";
import { enhance, EnhancerBuilder } from "@uniformdev/canvas";

export class CustomDataClient extends DefaultDataClient {
  protected override async enhanceRoute(
    options: EnhanceRouteOptions
  ): Promise<void> {
    if (options.source === "middleware") {
      // do not enhance in middleware, unless enhancing would result in new runnable (test, personalization) components.
      return;
    }

    enhance({
      composition: options.route.compositionApiResponse.composition,
      enhancers: new EnhancerBuilder(),
      context: {},
    });
  }
}
```

Pass an instance to the middleware handler:

```ts
export default (request: NextRequest) => {
  return handleUniformRoute({
    request,
    dataClient: new CustomDataClient(),
  });
};
```

Also pass the same data client instance to `UniformComposition`:

```ts
export default async function UniformPage(props: UniformPageParameters) {
  const { code } = await props.params;
  return (
    <UniformComposition
      code={code}
      resolveRoute={resolveRouteFromCode}
      resolveComponent={resolveComponent}
      clientContextComponent={CustomUniformClientContext}
      dataClient={new CustomDataClient()}
    />
  );
}
```

---

## Composition cache

The composition cache provides server-side access to the full `ComponentInstance` data within any component. This is useful when you need to access composition-level content (metadata parameters, page title, for example), or inspect the whole slot contents instead of just rendering slot components.

### Setting up the cache

```ts
// lib/cache.ts
import { createCompositionCache } from "@uniformdev/next-app-router";

export const compositionCache = createCompositionCache();
```

Pass the cache to `UniformComposition`:

```tsx
// app/uniform/[code]/page.tsx
import { compositionCache } from "@/lib/cache";

export default async function UniformPage(props: UniformPageParameters) {
  const { code } = await props.params;
  return (
    <UniformComposition
      code={code}
      resolveRoute={resolveRouteFromCode}
      resolveComponent={resolveComponent}
      compositionCache={compositionCache}
    />
  );
}
```

### Accessing component data

Within any component, use the cache to retrieve the full `ComponentInstance` for a slot child:

```tsx
import { compositionCache } from "@/lib/cache";
import { ComponentProps } from "@uniformdev/next-app-router/component";

type NavigationSlots = "links";

export const Navigation = ({ slots, context }: ComponentProps<unknown, NavigationSlots>) => {
  const linkData = slots.links.items.map((item) => {
    if (!item) return null;
    const resolved = compositionCache.getUniformComponent({
      componentId: item._id,
      compositionId: context._id,
    });
    return {
      type: resolved?.type,
      title: resolved?.parameters?.title?.value as string,
      url: resolved?.parameters?.url?.value as string,
    };
  });

  return (
    <nav>
      {linkData.map((link, i) =>
        link ? <a key={i} href={link.url}>{link.title}</a> : null
      )}
    </nav>
  );
};
```

The cache exposes three methods:

| Method | Description |
| --- | --- |
| `getUniformComposition({ id })` | Get the full root composition by ID |
| `setUniformComposition(composition)` | Store a composition (called internally) |
| `getUniformComponent({ compositionId, componentId })` | Get a specific component instance by ID |

---

## Enabling Cache Components

Next.js 16 introduces the notion of [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents), which lets the framework cache the result of server-side function calls. The Uniform SDK ships a cache-enabled version of `resolveRouteFromCode` that wraps route resolution in `'use cache'`, so subsequent requests for the same page state are served from cache without additional API calls.

### Step 1: Enable `cacheComponents` in Next.js config

```ts
// next.config.ts
import { withUniformConfig } from "@uniformdev/next-app-router/config";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default withUniformConfig(nextConfig);
```

### Step 2: Import `resolveRouteFromCode` from the cache path

In your `app/uniform/[code]/page.tsx`, change the import path:

```tsx
// Before (no caching):
// import { resolveRouteFromCode } from "@uniformdev/next-app-router";

// After (with use cache):
import { resolveRouteFromCode } from "@uniformdev/next-app-router/cache";
```

The rest of the page component remains identical -- only the import path changes. The cache-enabled `resolveRouteFromCode` internally uses `'use cache'` so the composition data is cached at the framework level and served on subsequent requests without re-fetching from the Uniform API.

> **Note:**
>
> In development mode, caching is automatically disabled so you always see fresh data. Cache is also bypassed when draft mode or in-context editing is active.

### Suspense and streaming

You can wrap individual components in `Suspense` boundaries via the `resolveComponent` function. This enables streaming: the page shell renders immediately while slower components load in asynchronously.

```ts
export const resolveComponent: ResolveComponentFunction = ({ component }) => {
  if (component.type === "hero") {
    return {
      component: HeroComponent,
      suspense: {
        fallback: () => <div className="h-64 animate-pulse bg-gray-200" />,
      },
    };
  }

  if (component.type === "page") {
    return { component: PageComponent };
  }

  return {
    component: ({ type }) => <div>Component not found: {type}</div>,
  };
};
```

When `suspense` is provided in the resolve result, the SDK wraps the component in a React `<Suspense>` boundary with the specified fallback. This is particularly useful for components that fetch additional data or perform async operations during server rendering.

---

## Server clients

The SDK provides server-only clients for accessing Uniform data directly:

```tsx
import {
  getCompositionDeliveryClient,
  getManifest,
  getManifestClient,
  getProjectMapClient,
  getRouteClient,
} from "@uniformdev/next-app-router";
```

### Examples

```tsx
// Fetch a specific composition by ID
import { CANVAS_PUBLISHED_STATE } from "@uniformdev/canvas";

const compositionClient = getCompositionDeliveryClient({
  cache: { type: "no-cache" },
});

const composition = await compositionClient.get({
  compositionId: "abc123",
});

// Get the personalization manifest
const manifest = await getManifest({
  state: CANVAS_PUBLISHED_STATE,
});

// Route resolution
const routeClient = getRouteClient({
  cache: { type: "force-cache" },
});
```

These clients are server-only (they import `'server-only'`) and cannot be used in client components. They return the same client classes documented on the utility SDK pages, pre-configured with Next.js caching; see the [Content API clients](https://docs.uniform.app/docs/sdk/content-api-clients), [Route Client](https://docs.uniform.app/docs/sdk/route-client), and [Project Map Client](https://docs.uniform.app/docs/sdk/project-map-client) pages for the full method reference.

---

## Adapter compatibility mode

If you're migrating from an older Uniform SDK or prefer a flattened props API where parameter values are spread directly onto component props (instead of nested under `parameters`), use the adapter compatibility layer:

```ts
// components/resolveComponent.ts
import { createAdapterResolveComponentFunction } from "@uniformdev/next-app-router/compat";
import { pageMapping } from "./page";

export const resolveComponent = createAdapterResolveComponentFunction({
  mappings: {
    page: pageMapping,
  },
});
```

```tsx
// components/page.tsx
import { ResolveComponentResultWithType } from "@uniformdev/next-app-router/compat";
import { ComponentProps } from "@uniformdev/next-app-router/component";

type PageProps = unknown;
type PageSlots = "content" | "header" | "footer";

const Page = (props: ComponentProps<PageProps, PageSlots>) => {
  return <div>Page</div>;
};

export const pageMapping: ResolveComponentResultWithType = {
  type: "page",
  component: Page,
  mode: "adapted",
};
```

In adapted mode, parameter values are extracted from `ComponentParameter` wrappers and spread directly onto the component props. The original parameters remain accessible via `component.parameters`.

`UniformText` and `UniformSlot` are also exported from `@uniformdev/next-app-router/compat`.

---

## Manifest management

**Next.js App Router does not require downloading (pulling) the manifest locally.** The manifest is fetched at runtime by the SDK.

The approach of pulling manifest into a local file and bundling it with the application codebase was the default approach with previous versions of Next.js SDK and is not recommended now.

In terms of manifest publishing, which you need to do when you sync manifest definitions to your project, you can _optionally_ enable mamifest publishing from your command line by adding this script to your `package.json`:

```json
{
  "scripts": {
    "uniform:publish": "uniform context manifest publish"
  }
}
```
