Next.js App Router SDK

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 15 with the Page Router? You can continue using the Next.js Page Router SDK, 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 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 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.

This SDK takes full advantage of Next.js 16 capabilities, most notably the cacheComponents support (learn more here 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 below for setup instructions.

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

  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.


  • Next.js 16+ (App Router)
  • Node.js 22+
  • A Uniform project with compositions set up

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:

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

Alternatively, if you prefer to add Uniform to an existing project manually, follow the steps below.

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:

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

Create a .env file in your project root:

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 Settings > API Keys. See the API keys guide for more information.


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.

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

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

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

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

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

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

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

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

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

// 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();

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

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


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.

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

PropTypeDescription
typestringThe component type identifier (e.g., "hero")
variantstring | undefinedActive variant ID when inside a personalization or test
parametersTParametersThe component's parameter values, each wrapped in ComponentParameter<T>
slotsRecord<TSlotNames, SlotDefinition>Child component slots
componentComponentContextComponent metadata (_id, _parentId, slotName, slotIndex)
contextCompositionContextComposition-level metadata (_id, type, state, isContextualEditing, matchedRoute, dynamicInputs)

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

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

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:

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 renders text parameters with built-in support for inline editing in the Uniform visual editor:

<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 renders rich text parameters with full formatting support:

<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 />

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

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

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

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

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

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

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

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.


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

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


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.

The simplest setup uses uniformMiddleware() with no options:

import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; export default uniformMiddleware();

For more control, use handleUniformRoute directly:

import { handleUniformRoute } from "@uniformdev/next-app-router/middleware"; import { NextRequest } from "next/server"; export default (request: NextRequest) => { return handleUniformRoute({ request }); };
OptionTypeDescription
rewriteRequestPath(options) => Promise<RewriteRequestPathResult>Transform the incoming request path before route resolution
rewriteDestinationPath(options) => Promise<string>Transform the output path after resolution
pathPatternsWithVariationsstring[]Paths that should pre-compute personalizations
release{ id: string }Content release ID to resolve against
quirksQuirksCustom quirks to inject into the visitor context
defaultConsentbooleanOverride the default consent setting
localestringLocale to use for route resolution

Inject custom quirks based on request data:

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

For localized sites, use rewriteRequestPath to prepend locale information:

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

If you determine the locale from cookies or headers:

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

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

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.

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.

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

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

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.

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

export const config = { matcher: ["/", "/about", "/products/:path*"], };
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, }); };

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

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

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

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:

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

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

Vercel HeaderQuirk Key
x-vercel-ip-countryvc-country
x-vercel-ip-country-regionvc-region
x-vercel-ip-cityvc-city

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


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.

Access the Uniform Context instance on the client:

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

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

"use client"; import { useQuirks } from "@uniformdev/next-app-router/component"; export const LocationBanner = () => { const quirks = useQuirks(); return <div>Current country: {quirks?.country ?? "Unknown"}</div>; };

Provides reactive access to the current visitor's score values:

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

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

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

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

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:

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:

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

Also pass the same data client instance to UniformComposition:

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

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.

// lib/cache.ts import { createCompositionCache } from "@uniformdev/next-app-router"; export const compositionCache = createCompositionCache();

Pass the cache to UniformComposition:

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

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

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:

MethodDescription
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

Next.js 16 introduces the notion of Cache Components, 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.

// 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);

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

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

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.

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.


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

import { getCanvasClient, getManifest, getManifestClient, getProjectMapClient, getRouteClient, } from "@uniformdev/next-app-router";
// Fetch a specific composition by ID import { CANVAS_PUBLISHED_STATE } from "@uniformdev/canvas"; const canvasClient = getCanvasClient({ cache: { type: "no-cache" }, }); const composition = await canvasClient.getCompositionById({ compositionId: "abc123", state: CANVAS_PUBLISHED_STATE, }); // 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.


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:

// components/resolveComponent.ts import { createAdapterResolveComponentFunction } from "@uniformdev/next-app-router/compat"; import { pageMapping } from "./page"; export const resolveComponent = createAdapterResolveComponentFunction({ mappings: { page: pageMapping, }, });
// 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.


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:

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