Upgrade from v1 to v2

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.

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.


Areav1v2
Packages@uniformdev/canvas-next-rsc, -client and -shared@uniformdev/next-app-router, -client and -shared
Next.js15.5.15 or later16.0.7 or later
Node.js18.18 or later20.9 or later
MiddlewareNone. The page resolves the route.Required. The middleware resolves the route and rewrites the request.
Route fileapp/[[...path]]/page.tsxapp/uniform/[code]/page.tsx
Playgroundapp/playground/page.tsxapp/playground/[code]/page.tsx
Client context<UniformContext> in the root layoutThe clientContextComponent prop of UniformComposition
Component propsParameter values at the top level, and the full ComponentInstanceParameters 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 configurationuniform.server.config and withUniformConfig are requiredThe two are optional
Personalization and testsevaluation in the server configurationThe middleware evaluates them. No configuration.

  1. Remove the v1 packages:

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

    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.


v1 and v2 both have withUniformConfig. Change the import:

next.config.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.


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

uniform.server.config.ts

import type { UniformServerConfig } from "@uniformdev/next-app-router/config"; const config: UniformServerConfig = { defaultConsent: true, quirkSerialization: true, playgroundPath: "/playground", }; export default config;
v1 optionv2
defaultConsentdefaultConsent
context.disableDevToolscontext.disableDevTools
experimental.quirkSerializationquirkSerialization (top level)
canvasCache, manifestCache, projectMapCacheRemoved. Use the cache option of the server clients in your own code.
evaluationRemoved. The middleware evaluates personalizations and tests.
pprRemoved
experimental.edgeRedirects, experimental.edgeCompositions, experimental.localeDynamicInputsRemoved
The playgroundPath option of the preview handlerplaygroundPath 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.


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

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), and Canvas preview shows published content. Keep middleware.ts and runtime: "experimental-edge".

For the options of the middleware, refer to Middleware configuration.


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

Before (v1):

app/[[...path]]/page.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

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:

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

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.

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

app/uniform/[code]/page.tsx

import { createUniformStaticParams } from "@uniformdev/next-app-router"; export const generateStaticParams = () => createUniformStaticParams({ paths: ["/", "/about"] });
v1v2
generateStaticParams, createStaticParams({ expand })createUniformStaticParams({ paths, rewrite, locale })
Returns { path: string[] } itemsReturns { code: string } items

For more information, refer to Static generation (ISR).


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

Before (v1):

app/layout.tsx

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

After (v2):

app/layout.tsx

{children}

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

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

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

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

Before (v1):

app/playground/page.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

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


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

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

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.


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

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

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 propv2 prop
Parameter values at the top levelparameters, with ComponentParameter<T> values. Read the value in .value.
component (the full ComponentInstance)component (ComponentContext: _id, _parentId, slotName and slotIndex)
slotName, slotIndexcomponent.slotName, component.slotIndex
context (with composition, path, searchParams, isDraftMode and previewMode)context (CompositionContext: _id, type, state, isContextualEditing, matchedRoute, dynamicInputs and pageState)
Nonetype and variant (string | undefined)

For the full types, refer to the SDK reference.

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

Before (v1):

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

After (v2):

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


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

Before (v1):

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

After (v2):

<UniformSlot slot={slots.header} />

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

v1v2
({ 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.


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

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

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


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.

uniform/resolve.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.

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

uniform/mappings/page.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.


  • [ ] 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.