Upgrade from v1 to v2

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 for assistance.


Areav1v2
Main package@uniformdev/canvas-next-rsc + several others@uniformdev/next-app-router (single package)
MiddlewareOptionalRequired
File structureapp/[[...slug]]/page.tsx (catch-all)app/uniform/[code]/page.tsx
Component propsParameters flattened to top-levelParameters in a parameters object with ComponentParameter<T> wrappers
Slots<UniformSlot name="slotName" /><UniformSlot slot={slots.slotName} />
Server configRequired (uniform.server.config.ts)Optional (sensible defaults)
Route clientgetDefaultRouteClientgetRouteClient

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

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

Your package.json should include:

{ "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.


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

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

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

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

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)

Create app/uniform/[code]/page.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} /> ); }

Create app/playground/[code]/page.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} /> ); }

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

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.


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.
type HeroParameters = { title: string; }; type HeroProps = ComponentProps<HeroParameters>; const Hero = ({ title }: HeroProps) => { return <h1>{title}</h1>; };
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>; };

For reference, the full v2 ComponentProps type is:

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.

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

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

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

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

The route client function has been renamed:

v1v2
getDefaultRouteClientgetRouteClient

Update any code that references the old function name.


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.

import { createAdapterResolveComponentFunction } from "@uniformdev/next-app-router/compat"; import * as mappings from "./mappings"; export const resolveComponent = createAdapterResolveComponentFunction({ mappings, });

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

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:

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.


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