# Upgrade from App Router SDK v1 to v2

> Move a Next.js App Router app from Uniform SDK v1 (@uniformdev/canvas-next-rsc) to v2 (@uniformdev/next-app-router). Packages, middleware, routes, preview, components, slots and clients.

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

This guide tells you how to move a Next.js App Router app from Uniform SDK v1 (`@uniformdev/canvas-next-rsc`) to v2 (`@uniformdev/next-app-router`), version 20.81.

> **Warning:**
>
> This guide is for apps that use the **App Router** with v1 of the SDK. If your app uses the **Pages Router** (`@uniformdev/canvas-next`), [contact support](https://uniform.dev/support).

> **Developer Preview:**
>
> This guide moves an app to version 20.81 of `@uniformdev/next-app-router`. To use the developer preview (`20.81.1-alpha.25.sha-f076f9b857`), do the steps in this guide first. Then do the steps in [Upgrade to the developer preview](https://docs.uniform.app/docs/sdk/nextjs-app-router/upgrade-to-developer-preview).

---

## Overview of changes

| Area | v1 | v2 |
| --- | --- | --- |
| Packages | `@uniformdev/canvas-next-rsc`, `-client` and `-shared` | `@uniformdev/next-app-router`, `-client` and `-shared` |
| Next.js | 15.5.15 or later | 16.0.7 or later |
| Node.js | 18.18 or later | 20.9 or later |
| Middleware | None. The page resolves the route. | Required. The middleware resolves the route and rewrites the request. |
| Route file | `app/[[...path]]/page.tsx` | `app/uniform/[code]/page.tsx` |
| Playground | `app/playground/page.tsx` | `app/playground/[code]/page.tsx` |
| Client context | `<UniformContext>` in the root layout | The `clientContextComponent` prop of `UniformComposition` |
| Component props | Parameter values at the top level, and the full `ComponentInstance` | Parameters in `parameters`, as `ComponentParameter<T>` |
| Slots | `<UniformSlot data={component} context={context} slot={slots.header} />` | `<UniformSlot slot={slots.header} />` |
| Text | `<UniformText component={component} context={context} parameterId="title" />` | `<UniformText component={component} parameter={title} />` |
| Server configuration | `uniform.server.config` and `withUniformConfig` are required | The two are optional |
| Personalization and tests | `evaluation` in the server configuration | The middleware evaluates them. No configuration. |

---

## Step 1: Update the packages

1. Remove the v1 packages:

   ```bash
   npm uninstall @uniformdev/canvas-next-rsc @uniformdev/canvas-next-rsc-client @uniformdev/canvas-next-rsc-shared
   ```
2. Install v2, and Next.js 16:

   ```bash
   npm install @uniformdev/next-app-router@^20.81.0 next@^16
   ```

`@uniformdev/next-app-router-client` and `@uniformdev/next-app-router-shared` install as dependencies of `@uniformdev/next-app-router`. Add them to `package.json` only when your code imports them.

> **Note:**
>
> Use the same version for all `@uniformdev` packages in `package.json`, for example `@uniformdev/canvas` and `@uniformdev/context`. Different versions can install two copies of `@uniformdev/context`.

v2 also depends on `@uniformdev/canvas-react`. Remove `@uniformdev/canvas-react` from `package.json` only if your code does not import it.

---

## Step 2: Update the Next.js configuration

v1 and v2 both have `withUniformConfig`. Change the import:

`next.config.ts`

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

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

export default withUniformConfig(nextConfig);
```

In v2, `withUniformConfig` is optional. It connects `uniform.server.config` to the SDK when the file exists. Without the file, the SDK uses its default configuration.

---

## Step 3: Update the server configuration

v2 does not use most v1 options. Change `uniform.server.config.ts` as follows:

`uniform.server.config.ts`

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

const config: UniformServerConfig = {
  defaultConsent: true,
  quirkSerialization: true,
  playgroundPath: "/playground",
};

export default config;
```

| v1 option | v2 |
| --- | --- |
| `defaultConsent` | `defaultConsent` |
| `context.disableDevTools` | `context.disableDevTools` |
| `experimental.quirkSerialization` | `quirkSerialization` (top level) |
| `canvasCache`, `manifestCache`, `projectMapCache` | Removed. Use the `cache` option of the [server clients](https://docs.uniform.app/docs/sdk/nextjs-app-router/server-clients) in your own code. |
| `evaluation` | Removed. The middleware evaluates personalizations and tests. |
| `ppr` | Removed |
| `experimental.edgeRedirects`, `experimental.edgeCompositions`, `experimental.localeDynamicInputs` | Removed |
| The `playgroundPath` option of the preview handler | `playgroundPath` in the server configuration |

> **Warning:**
>
> Your file replaces the default configuration fully. The SDK does not merge the two. Thus always set these options in your file:
>
> - `defaultConsent`. Without it, the default consent is `false`.
> - `playgroundPath`. Without it, the middleware does not send pattern previews to the playground route.

The default configuration of version 20.81 is `defaultConsent: true`, `quirkSerialization: true`, `middlewareRuntimeCache: true` and `playgroundPath: "/uniform/playground"`. If your app uses the default playground route (`app/uniform/playground/[code]`) and these values, you can remove the file.

---

## Step 4: Add the middleware

v1 has no middleware. In v2, the middleware is required. It finds the route of each request, applies Uniform redirects, and rewrites the request to `/uniform/[code]`.

Make `middleware.ts` in the project root:

`middleware.ts`

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

> **Keep middleware.ts on the edge runtime:**
>
> Next.js 16 renames `middleware.ts` to `proxy.ts`, and shows a deprecation warning for `middleware.ts`. Do not rename the file. On Vercel, the SDK cannot read draft mode in `proxy.ts` ([vercel/next.js#82344](https://github.com/vercel/next.js/issues/82344)), and Canvas preview shows published content. Keep `middleware.ts` and `runtime: "experimental-edge"`.

For the options of the middleware, refer to [Middleware configuration](https://docs.uniform.app/docs/sdk/nextjs-app-router/middleware).

---

## Step 5: Replace the route file

1. Delete `app/[[...path]]/page.tsx`.
2. Make `app/uniform/[code]/page.tsx`.

**Before (v1):**

`app/[[...path]]/page.tsx`

```tsx
import {
  createServerUniformContext,
  PageParameters,
  retrieveRoute,
  UniformComposition,
} from "@uniformdev/canvas-next-rsc";
import { resolveComponent } from "@/uniform/resolve";

export default async function HomePage(props: PageParameters) {
  const route = await retrieveRoute(props);
  const serverContext = await createServerUniformContext({ searchParams: await props.searchParams });

  return (
    <UniformComposition
      {...props}
      route={route}
      resolveComponent={resolveComponent}
      serverContext={serverContext}
      mode="server"
    />
  );
}
```

**After (v2):**

`app/uniform/[code]/page.tsx`

```tsx
import {
  resolveRouteFromCode,
  UniformComposition,
  type UniformPageParameters,
} from "@uniformdev/next-app-router";
import { resolveComponent } from "@/uniform/resolve";
import { UniformClientContext } from "@/uniform/clientContext";

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

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

These parts of v1 are removed:

| v1 | v2 |
| --- | --- |
| `retrieveRoute(props)` | The middleware resolves the route. The page gets the `code`. |
| `PageParameters` (`params.path`) | `UniformPageParameters` (`params.code`) |
| `mode="server"` and `mode="static"` | Removed |
| `createServerUniformContext` | Removed. To add quirks on the server, use the `quirks` option of the middleware. |
| `ContextUpdateTransfer` with `serverContext` | Removed. `@uniformdev/next-app-router-client` has a client `ContextUpdateTransfer` that takes only `update`. |
| `retrieveCompositionBySlug`, `resolveComposition` | Removed |

In version 20.81, the middleware applies Uniform redirects, and rewrites a missing route to `/404`. `UniformComposition` calls `notFound()` when the code has no composition.

### Static params

v1 got all paths from the project map. v2 takes a list of paths:

`app/uniform/[code]/page.tsx`

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

export const generateStaticParams = () => createUniformStaticParams({ paths: ["/", "/about"] });
```

| v1 | v2 |
| --- | --- |
| `generateStaticParams`, `createStaticParams({ expand })` | `createUniformStaticParams({ paths, rewrite, locale })` |
| Returns `{ path: string[] }` items | Returns `{ code: string }` items |

For more information, refer to [Static generation (ISR)](https://docs.uniform.app/docs/sdk/nextjs-app-router/incremental-static-regeneration-isr).

---

## Step 6: Remove UniformContext from the root layout

In v1, the root layout renders `<UniformContext>`. In v2, `UniformComposition` renders the Uniform Context for you.

**Before (v1):**

`app/layout.tsx`

```tsx
<UniformContext clientContextComponent={UniformClientContext}>{children}</UniformContext>
```

**After (v2):**

`app/layout.tsx`

```tsx
{children}
```

Give your client context component to `UniformComposition` (Step 5) and to `UniformPlayground` (Step 7).

### Update the client context component

The client context hooks have the same names in v2. Change the import to `@uniformdev/next-app-router/component`, and give `compositionMetadata` to `useInitUniformContext`:

`uniform/clientContext.tsx`

```tsx
"use client";

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

export const UniformClientContext: 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;
};
```

---

## Step 7: Replace the playground page

1. Delete `app/playground/page.tsx`.
2. Make `app/playground/[code]/page.tsx`.

**Before (v1):**

`app/playground/page.tsx`

```tsx
import { UniformPlayground, type UniformPlaygroundProps } from "@uniformdev/canvas-next-rsc";
import { resolveComponent } from "@/uniform/resolve";

export default function PlaygroundPage(props: { searchParams: UniformPlaygroundProps["searchParams"] }) {
  return <UniformPlayground {...props} resolveComponent={resolveComponent} />;
}
```

**After (v2):**

`app/playground/[code]/page.tsx`

```tsx
import {
  type PlaygroundParameters,
  resolvePlaygroundRoute,
  UniformPlayground,
} from "@uniformdev/next-app-router";
import { resolveComponent } from "@/uniform/resolve";
import { UniformClientContext } from "@/uniform/clientContext";

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

The middleware sends pattern previews to `${playgroundPath}/[code]`. This page is at `/playground/[code]`, so `playgroundPath` must be `"/playground"` (Step 3).

---

## Step 8: Update the preview route

Move the `playgroundPath` option of the preview handler to the server configuration (Step 3), and change the import:

**Before (v1):**

`app/api/preview/route.ts`

```ts
import {
  createPreviewGETRouteHandler,
  createPreviewOPTIONSRouteHandler,
  createPreviewPOSTRouteHandler,
} from "@uniformdev/canvas-next-rsc/handler";

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

**After (v2):**

`app/api/preview/route.ts`

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

export const GET = createPreviewGETRouteHandler();
export const POST = createPreviewPOSTRouteHandler();
export const OPTIONS = createPreviewOPTIONSRouteHandler();
```

In v2, the handler finds a pattern preview from the `is_incontext_editing_playground` query string, or from an `id` without a `path`. Use `resolveFullPath` only when your URLs are different from the project map paths.

---

## Step 9: Update the component props

v1 gives the parameter values at the top level of the props. v2 gives them in `parameters`, and wraps each value in `ComponentParameter<T>`.

**Before (v1):**

```tsx
import type { ComponentProps } from "@uniformdev/canvas-next-rsc/component";

type HeroParameters = {
  title?: string;
};

export const Hero = ({ title }: ComponentProps<HeroParameters>) => <h1>{title}</h1>;
```

**After (v2):**

`components/hero.tsx`

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

type HeroParameters = {
  title?: ComponentParameter<string>;
};

export const Hero = ({ parameters: { title } }: ComponentProps<HeroParameters>) => <h1>{title?.value}</h1>;
```

The other props also changed:

| v1 prop | v2 prop |
| --- | --- |
| Parameter values at the top level | `parameters`, with `ComponentParameter<T>` values. Read the value in `.value`. |
| `component` (the full `ComponentInstance`) | `component` (`ComponentContext`: `_id`, `_parentId`, `slotName` and `slotIndex`) |
| `slotName`, `slotIndex` | `component.slotName`, `component.slotIndex` |
| `context` (with `composition`, `path`, `searchParams`, `isDraftMode` and `previewMode`) | `context` (`CompositionContext`: `_id`, `type`, `state`, `isContextualEditing`, `matchedRoute`, `dynamicInputs` and `pageState`) |
| None | `type` and `variant` (`string \| undefined`) |

For the full types, refer to the [SDK reference](https://docs.uniform.app/docs/sdk/nextjs-app-router/reference#type-definitions).

### UniformText and UniformRichText

`UniformText` and `UniformRichText` take the parameter object, not its ID. They do not take `context`.

**Before (v1):**

```tsx
<UniformText component={component} context={context} parameterId="title" as="h1" />
<UniformRichText component={component} parameterId="description" />
```

**After (v2):**

```tsx
{title ? <UniformText component={component} parameter={title} as="h1" /> : null}
{description ? <UniformRichText component={component} parameter={description} /> : null}
```

The `parameter` prop is required, so render the component only when the parameter exists.

---

## Step 10: Update the slots

Slots were already in the `slots` prop in v1. In v2, `UniformSlot` does not take `data` and `context`.

**Before (v1):**

```tsx
<UniformSlot data={component} context={context} slot={slots.header} />
```

**After (v2):**

```tsx
<UniformSlot slot={slots.header} />
```

The render function of the children changed too. It gets `_id` in place of `component`:

| v1 | v2 |
| --- | --- |
| `({ child, component, key, slotName, slotIndex })` | `({ child, _id, key, slotName, slotIndex })` |

An empty slot renders `null` in v2, not an empty array. To get the items as an array, use `getUniformSlot` from `@uniformdev/next-app-router/component`.

---

## Step 11: Update the clients

v1 and v2 both have `getRouteClient({ cache })`. The `getDefault*` clients of v1 are removed:

| v1 | v2 |
| --- | --- |
| `getDefaultRouteClient({ searchParams })` | `getRouteClient({ state })` or `getRouteClient({ searchParams, draftModeEnabled })` |
| `getDefaultCompositionDeliveryClient({ searchParams })` | `getCompositionDeliveryClient(…)` with the same options |
| `getDefaultCanvasClient({ searchParams })` | `getCompositionDeliveryClient(…)`. `getCanvasClient` is deprecated. |
| `getDefaultManifestClient({ searchParams })` | `getManifestClient(…)` with the same options |
| `getDefaultProjectMapClient({ searchParams })` | `getProjectMapClient(…)` with the same options. The options are required. |
| `getManifest({ searchParams })` | `getManifest({ state })` or `getManifest({ searchParams, draftModeEnabled })` |

In v2, `searchParams` is a `URLSearchParams` object, not a plain object. For more information, refer to [Call Uniform APIs on the server](https://docs.uniform.app/docs/sdk/nextjs-app-router/server-clients).

These v1 helpers are removed: `isDraftModeEnabled`, `isIncontextEditingEnabled`, `isDevelopmentEnvironment` and `isOnVercelPreviewEnvironment`. Use `draftMode()` from `next/headers` to find draft mode.

---

## Step 12: Use the adapter layer (optional)

The adapter layer gives the parameter values at the top level of the props, as in v1. Use it to move your components to v2 one at a time.

### Set up the adapter resolver

`uniform/resolve.tsx`

```tsx
import { createAdapterResolveComponentFunction } from "@uniformdev/next-app-router/compat";
import * as mappings from "./mappings";

export const resolveComponent = createAdapterResolveComponentFunction({ mappings });
```

The SDK finds a mapping by its `type` field, not by its key. Thus `import * as mappings` from the v1 starter works.

### Adapt individual components

Add `mode: "adapted"` to each mapping that you did not move to v2 yet:

`uniform/mappings/page.tsx`

```tsx
import type { ComponentProps, ResolveComponentResultWithType } from "@uniformdev/next-app-router/compat";
import { UniformSlot } from "@uniformdev/next-app-router/component";

type PageParameters = {
  title?: string;
};

const Page = ({ title, slots }: ComponentProps<PageParameters, "content">) => (
  <main>
    <h1>{title}</h1>
    <UniformSlot slot={slots.content} />
  </main>
);

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

An adapted component gets these props:

- The parameter values at the top level, as in v1.
- `component`: the v2 `ComponentContext` and the `parameters` objects. It is not the v1 `ComponentInstance`.
- `type`, `variant`, `slots` and `context` from v2.

Thus change these parts of a v1 component, also in the adapted mode:

- `UniformSlot`: import it from `@uniformdev/next-app-router/component`, and remove `data` and `context`.
- `UniformText`: import it from `@uniformdev/next-app-router/compat`. It takes `component` and `parameterId`, but not `context`.
- `UniformRichText`: there is no adapted version. Use the v2 `UniformRichText` with `parameter={component.parameters.description}`.
- `component.type`, `component.slots` and `component.variant`: use `type`, `slots` and `variant`.
- `slotName` and `slotIndex`: use `component.slotName` and `component.slotIndex`.
- `context`: use the v2 fields. The v1 fields `composition`, `path`, `searchParams`, `isDraftMode` and `previewMode` are removed.

A type that has no mapping renders "Not implemented". The v1 `DefaultNotImplementedComponent` is removed.

---

## Migration checklist

- [ ] Remove `@uniformdev/canvas-next-rsc`, `-client` and `-shared`. Install `@uniformdev/next-app-router` and Next.js 16.
- [ ] Change the import of `withUniformConfig` to `@uniformdev/next-app-router/config`.
- [ ] Remove the v1 options from `uniform.server.config.ts`. Set `defaultConsent` and `playgroundPath`.
- [ ] Add `middleware.ts` with `uniformMiddleware()` and `runtime: "experimental-edge"`.
- [ ] Replace `app/[[...path]]/page.tsx` with `app/uniform/[code]/page.tsx`.
- [ ] Replace `createStaticParams` with `createUniformStaticParams`.
- [ ] Remove `<UniformContext>` from the root layout. Give `clientContextComponent` to `UniformComposition` and `UniformPlayground`.
- [ ] Replace `app/playground/page.tsx` with `app/playground/[code]/page.tsx`.
- [ ] Remove the `playgroundPath` option from the preview handler.
- [ ] Change the component props to `parameters` and `ComponentParameter<T>`, or use the adapter layer.
- [ ] Change `UniformText` and `UniformRichText` from `parameterId` to `parameter`.
- [ ] Remove `data` and `context` from `UniformSlot`.
- [ ] Replace the `getDefault*` clients.
- [ ] Do a test of the preview and visual editing in Canvas.
- [ ] Do a test of the personalizations and A/B tests.
