Recipe: add Uniform regions to Next.js pages
Developer preview
Goal: your app has its pages in Next.js already. The pages stay ordinary Next.js routes, with their layout in JSX. Uniform manages only some named regions in each page, for example a hero region and a banner region. Authors edit these regions in Canvas.
Use this recipe to add Uniform to an app one page at a time. If Uniform must own the full layout of the page, use the app/uniform/[code] route. Refer to Add the composition route.
The idea: two trees, joined by slot names#
A page has two trees:
- The page tree: Next.js renders it from your JSX. It contains
UniformRegionelements, each with a name. - The composition tree: Uniform delivers it. Its root component has one slot for each region.
The slot name joins one region to one slot:
- The root component of the composition is only a holder for slots. It has no markup of its own.
- The region sets where the content goes. The slot sets what content goes there.
- A region can be at any depth of the page tree. The layout components between the page and the region do not pass slot data.
Prerequisites#
- A Next.js 16 App Router app with the Uniform SDK installed and configured. Refer to Next.js App Router SDK.
- The page state helper
lib/uniform/getPageState.ts. Copy it from Use ordinary Next.js routes. - In Uniform:
- A composition type for the pages, for example
page. Give it one slot for each region, for exampleheroandrelated. Write down the public IDs of the slots. - The component types that authors can put in the slots.
- A project map node for each Next.js route that has regions. For example, the node
/products/:slugforapp/products/[slug]/page.tsx. - A composition of the page type on each node.
- A composition type for the pages, for example
Step 1: Make the slot store#
The regions get their slots from a per-request store. Thus the layout does not pass the slots down:
lib/uniform/regions.ts
serverContext uses React cache. The value is for one server request only, and only server components can read it.
Step 2: Make the wrapper#
UniformRegions connects a page to its composition. The page JSX is its children:
components/uniform/UniformRegions.tsx
The composition renders one time. Root replaces its root component:
- The SDK renders the slots of the root, and gives them to
Rootas theslotsprop. Rootputs the slots in the store.Rootreturns the page JSX. EachUniformRegionin the JSX then reads its slot from the store.
Render the composition one time
Do not render one UniformResolvedComposition for each region. In Canvas, each render adds editing markers for the root component. With more than one set of markers, Canvas cannot select the components correctly.
note
Do not name the wrapper UniformComposition. The SDK exports a component with that name.
Step 3: Make the region#
components/uniform/UniformRegion.tsx
UniformRegion is a server component. Put it only below UniformRegions in the tree, so that the store has the slots before the region renders.
Step 4: Resolve the route on the page#
lib/uniform/resolvePageRoute.ts
getPageState makes the page state for the request:
- Published: it does not read
searchParams, so the route stays static. It setsedgeMode: truefor the edge middleware. - Draft: it reads
searchParamsto find the editor state and the release.
The page does not call notFound() when Uniform has no composition. The page renders, and its regions stay empty. If the page must be a 404 without a composition, call notFound() from next/navigation in the page.
Step 5: Add the regions to a page#
Wrap the JSX of the page in UniformRegions. Put a UniformRegion where each region goes:
app/products/[slug]/page.tsx
The
nameof each region must be the same as the public ID of a slot. A different name gives an empty region, with no error.The path that you give to
resolvePageRoutemust match a project map node. Here,/products/shoesmatches the node/products/:slug.To set the page title from Uniform, refer to SEO. Call
getUniformMetadataonly for a composition result:app/products/[slug]/page.tsx
import type { Metadata } from "next"; import { getUniformMetadata } from "@/lib/uniform/getUniformMetadata"; export async function generateMetadata({ params, searchParams }: Props): Promise<Metadata> { const { slug } = await params; const { pageState, route } = await resolvePageRoute(`/products/${slug}`, searchParams); return route?.type === "composition" ? getUniformMetadata({ pageState, route }) : { title: `Product ${slug}` }; }
Step 6: Keep the page type out of resolveComponent#
resolveComponent maps only the components that authors can put in the slots. Do not add the page type (page) or page parts, for example the header and the footer:
UniformRegionsreplaces the root component. ThusresolveComponentdoes not get the page type.- If the header is in the map, an author can put a second header into a region.
Step 7: Configure the middleware#
The middleware must rewrite each request to its own URL, not to /uniform/[code]. Then Next.js uses your routes.
Edge mode: use
handleUniformEdgeRoute. Make the manifest provider one time, at module scope, so that it keeps the manifest between requests. For the full code, refer to Configure the middleware and the preview.Lite mode: use
handleUniformRoute, and removeedgeMode: truefromgetPageState:middleware.ts
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware"; import type { NextRequest } from "next/server"; export default function middleware(request: NextRequest) { return handleUniformRoute({ request, // Rewrite each request to its own URL, so Next.js uses your routes. // An empty string keeps the default rewrite of the playground. rewriteDestinationPath: async ({ source }) => source === "route" ? `${request.nextUrl.pathname}${request.nextUrl.search}` : "", }); } export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"], runtime: "experimental-edge", };
The page makes its own page state. Thus the page does not read the code of the middleware. In lite mode, the middleware still sends the quirks from the Vercel geolocation headers to the browser.
Step 8: Configure the preview#
app/api/preview/route.ts
- Canvas sends the path of the project map node. If your URLs are different from the project map paths, change the path in
resolveFullPath. Refer to Change the preview path. - Each project map node that has a composition must have a Next.js route for its path. Without a route, the path gives a 404, also in Canvas preview.
Step 9: Show page patterns in the playground#
Canvas shows composition patterns on the playground route. A pattern of the page type has no component of its own, and resolveComponent does not know the page type. Thus render it through UniformRegions, with one region for each slot:
app/playground/[code]/page.tsx
Set playgroundPath: "/playground" in uniform.server.config.ts. Without this branch, a page pattern shows the fallback of your resolveComponent, for example "Component not found: page".
How it works#
- The middleware rewrites the request to its own URL. Next.js renders your route.
- The page makes the page state, and resolves the route from the path with
resolveRouteFromPath. - A Uniform redirect gives a redirect. No composition gives the page with empty regions.
UniformRegionsrendersUniformContextand the composition one time. The root component puts the slots in the store, and returns the page JSX.- Each
UniformRegionreads its slot from the store, and renders the slot items. - In Canvas, the regions show draft content with editing markers.
Limits#
- Authors cannot change the layout. They can fill only the regions that you put in the page.
- The slot names are a contract. A region name must be the same as a slot public ID. A mismatch gives an empty region, with no error.
- One composition for each page. For content from a second composition, for example a shared footer, refer to Render more than one composition.
- Server components only.
UniformRegionsandUniformRegionuseserverContext. A client component cannot read the slots. It can render aUniformRegionthat a server component gives to it aschildren. - Two route calls.
generateMetadataand the page both resolve the route. For published content, Next.js can serve the second call from its data cache.