# Upgrade from App Router SDK v1 to v2

> Step-by-step guide to upgrading your Next.js App Router project from the Uniform App Router SDK v1 to v2, covering package changes, middleware, file structure, components, and slots.

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

This guide walks you through upgrading an existing Next.js App Router project from Uniform SDK v1 (`@uniformdev/canvas-next-rsc`) to v2 (`@uniformdev/next-app-router`). The v2 SDK requires **Next.js 16** and introduces a new middleware-based architecture, a new file structure, and updated component APIs.

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

---

## Overview of changes

| Area | v1 | v2 |
| --- | --- | --- |
| **Main package** | `@uniformdev/canvas-next-rsc` + several others | `@uniformdev/next-app-router` (single package) |
| **Middleware** | Optional | Required |
| **File structure** | `app/[[...slug]]/page.tsx` (catch-all) | `app/uniform/[code]/page.tsx` |
| **Component props** | Parameters flattened to top-level | Parameters in a `parameters` object with `ComponentParameter<T>` wrappers |
| **Slots** | `<UniformSlot name="slotName" />` | `<UniformSlot slot={slots.slotName} />` |
| **Server config** | Required (`uniform.server.config.ts`) | Optional (sensible defaults) |
| **Route client** | `getDefaultRouteClient` | `getRouteClient` |

---

## Step 1: Update packages

Remove the old v1 packages and install the new v2 package:

```bash
npm uninstall @uniformdev/canvas-next-rsc @uniformdev/canvas-react @uniformdev/canvas-next
npm install @uniformdev/next-app-router@latest
```

Your `package.json` should include:

```json
{
  "dependencies": {
    "@uniformdev/next-app-router": "20.48.0"
  }
}
```

> **Note:**
>
> It's best to reference `@uniformdev/next-app-router` in your `package.json` to avoid version mismatches as new packages are released.

---

## Step 2: Add middleware

Middleware is now **required** to route requests and evaluate compositions. Create `middleware.ts` in your project root:

> **Why middleware.ts instead of proxy.ts?:**
>
> This recipe uses **middleware** at the project root rather than a proxy-based setup. It is a workaround for a [current Next.js limitation](https://github.com/vercel/next.js/issues/82344): draft mode is not accurate in middleware when using the Node.js runtime (e.g. in a proxy). Using `middleware.ts` with the edge runtime avoids this, so Uniform preview and draft mode work correctly.

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

export default uniformMiddleware();

// IMPORTANT: runtime: "experimental-edge" is required for preview
// to work correctly in Next.js 16
export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
  runtime: "experimental-edge",
};
```

### Custom rewrites (optional)

If you need to code-split your components and avoid defining them all on every page, you can customize where the middleware rewrites to:

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

export default (request: NextRequest) => {
  return handleUniformRoute({
    request,
    rewriteDestinationPath: async ({ code, source }) => {
      if (source === "route") {
        return `/rewrite-here-instead/${code}`;
      }

      return `/default-rewrite/${code}`;
    },
  });
};
```

---

## Step 3: Update file structure

The catch-all route pattern has changed. Replace your old `app/[[...slug]]/page.tsx` with the new structure:

**Before (v1):**

```
app/
├── [[...slug]]/
│   └── page.tsx          # Catch-all route
```

**After (v2):**

```
app/
├── uniform/
│   └── [code]/
│       └── page.tsx      # Main composition route
├── playground/
│   └── [code]/
│       └── page.tsx      # Playground route (visual editing)
```

### Main composition page

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

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

export const generateStaticParams = async () => {
  return createUniformStaticParams({
    // For localized sites, add locale prefixes:
    // paths: ["/en"],
    paths: ["/"],
  });
};

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

### Playground page

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

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

export const generateStaticParams = async () => {
  return createUniformPlaygroundStaticParams({
    // For localized sites:
    // paths: ["/en"],
    paths: ["/"],
  });
};

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

---

## Step 4: Update server config (optional)

If you have a `uniform.server.config.ts` or `uniform.server.config.js`, you can **remove it** if it matches the following defaults:

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

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

export default config;
```

If your config differs from the defaults, keep the file and update the import path from the old package to `@uniformdev/next-app-router/config`.

---

## Step 5: Update component props

The `ComponentProps` type has been reworked. The two key changes are:

1. **Parameters are no longer spread to the top level** -- they are now in a `parameters` object.
2. **Parameter values are wrapped in `ComponentParameter<T>`** -- access the value with `.value`.

### Before (v1)

```tsx
type HeroParameters = {
  title: string;
};

type HeroProps = ComponentProps<HeroParameters>;

const Hero = ({ title }: HeroProps) => {
  return <h1>{title}</h1>;
};
```

### After (v2)

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

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

type HeroProps = ComponentProps<HeroParameters>;

const Hero = ({ parameters: { title } }: HeroProps) => {
  return <h1>{title.value}</h1>;
};
```

### Full v2 ComponentProps type

For reference, the full v2 `ComponentProps` type is:

```ts
type ComponentProps<
  TParameters extends Record<string, ComponentParameter> | unknown =
    Record<string, ComponentParameter>,
  TSlotNames extends string = string,
> = {
  type: string;
  variant: string | null;
  slots: Record<TSlotNames, SlotDefinition>;
  parameters: TParameters;
  component: ComponentContext;
  context: CompositionContext;
};
```

Key differences from v1:

- You are no longer passed the entire component instance directly -- use the `component` (`ComponentContext`) object instead.
- The `slotName` and `slotIndex` properties have been removed from the top level.

---

## Step 6: Update slots

Slots are now passed as objects in the `slots` prop rather than being rendered by name.

### Before (v1)

```tsx
<UniformSlot name="header" />
<UniformSlot name="content" />
```

### After (v2)

```tsx
const Page = ({ slots }: ComponentProps<PageProps, "header" | "content">) => {
  return (
    <div>
      <UniformSlot slot={slots.header} />
      <UniformSlot slot={slots.content} />
    </div>
  );
};
```

### Accessing slot items programmatically

If you need to extract the rendered components from a slot:

```tsx
const headerItems = slots.header.items.map((item) => item?.component);
```

---

## Step 7: Update client helpers

The route client function has been renamed:

| v1 | v2 |
| --- | --- |
| `getDefaultRouteClient` | `getRouteClient` |

Update any code that references the old function name.

---

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

If you have many components and want to migrate incrementally, the v2 SDK provides an adapter layer that lets you use v1-style component definitions alongside v2 components.

### Set up the adapter resolver

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

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

### Adapt individual components

Add `mode: 'adapted'` to each component mapping that you haven't fully migrated yet:

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

type PageComponentParameters = {
  text: string;
};

const PageComponent = ({ text }: ComponentProps<PageComponentParameters>) => {
  return <div>{text}</div>;
};

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

When using the adapter layer, use `UniformText` and `UniformSlot` from the `compat` export:

```tsx
import { UniformText, UniformSlot } from "@uniformdev/next-app-router/compat";
```

This allows you to migrate components one at a time from the adapted (v1-style) API to the native v2 API.

---

## Migration checklist

Use this checklist to track your progress:

- [ ] Install `@uniformdev/next-app-router` and remove old packages
- [ ] Add `middleware.ts` with `uniformMiddleware()`
- [ ] Move from `app/[[...slug]]/page.tsx` to `app/uniform/[code]/page.tsx`
- [ ] Add `app/playground/[code]/page.tsx`
- [ ] Update or remove `uniform.server.config.ts`
- [ ] Update `ComponentProps` to use `parameters` object and `ComponentParameter<T>`
- [ ] Update `UniformSlot` from `name` prop to `slot` prop
- [ ] Rename `getDefaultRouteClient` to `getRouteClient`
- [ ] Update `next.config.ts` to use `withUniformConfig`
- [ ] Test visual editing in the Uniform dashboard
- [ ] Test personalization and A/B testing
