# Add Uniform regions to existing Next.js pages with the Next.js App Router SDK

> Recipe: keep your pages as ordinary Next.js routes, and let Uniform manage only some named regions in them. One composition per page, joined to the page by slot names.

Source: https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes/hybrid-pages

**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](https://docs.uniform.app/docs/sdk/nextjs-app-router/tutorial#step-10-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 `UniformRegion` elements, 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:

```mermaid
flowchart LR
  subgraph next["Page tree (Next.js)"]
    direction TB
    P["app/products/[slug]/page.tsx"] --> W["UniformRegions"]
    W --> H["Header"]
    W --> M["Main"]
    M --> R1(["UniformRegion name=hero"])
    M --> R2(["UniformRegion name=related"])
    W --> F["Footer"]
  end
  subgraph uni["Composition tree (Uniform)"]
    direction TB
    C["page (root)"] --> S1["slot: hero"]
    C --> S2["slot: related"]
    S1 --> B1["hero component"]
    S2 --> B2["card x 3"]
  end
  S1 -. "hero" .-> R1
  S2 -. "related" .-> R2
```

- 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](https://docs.uniform.app/docs/sdk/nextjs-app-router).
- The page state helper `lib/uniform/getPageState.ts`. Copy it from [Use ordinary Next.js routes](https://docs.uniform.app/docs/sdk/nextjs-app-router/resolving-compositions#use-ordinary-next-js-routes).
- In Uniform:

  - A composition type for the pages, for example `page`. Give it one slot for each region, for example `hero` and `related`. 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/:slug` for `app/products/[slug]/page.tsx`.
  - A composition of the page type on each node.

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

```ts
import { serverContext } from "@uniformdev/next-app-router";
import type { SlotDefinition } from "@uniformdev/next-app-router-shared";

// The slots of the page composition, for one request.
// UniformRegions sets them. Each UniformRegion reads them.
export const [getRegionSlots, setRegionSlots] = serverContext<Record<string, SlotDefinition | undefined>>({});
```

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

```tsx
import {
  UniformContext,
  UniformResolvedComposition,
  type ResolveComponentFunction,
  type ResolvedRouteResult,
  type UniformResolvedCompositionProps,
} from "@uniformdev/next-app-router";
import type { SlotDefinition } from "@uniformdev/next-app-router-shared";
import type { ReactNode } from "react";
import { resolveComponent } from "@/components/resolveComponent";
import { setRegionSlots } from "@/lib/uniform/regions";

type UniformRegionsProps = {
  result: ResolvedRouteResult;
  children: ReactNode;
  resolveEmptyPlaceholder?: UniformResolvedCompositionProps["resolveEmptyPlaceholder"];
};

export function UniformRegions({ result, children, resolveEmptyPlaceholder }: UniformRegionsProps) {
  // No composition for this path: render the page with empty regions.
  if (result.route?.type !== "composition") {
    return <UniformContext result={result}>{children}</UniformContext>;
  }

  const rootId = result.route.compositionApiResponse.composition._id;

  // Replaces the root component of the composition.
  const Root = ({ slots }: { slots?: Record<string, SlotDefinition> }) => {
    setRegionSlots(slots ?? {}); // 1. Give the slots to the regions.
    return children; // 2. Render the page.
  };

  const resolveRegionComponent: ResolveComponentFunction = (options) =>
    options.component._id === rootId ? { component: Root } : resolveComponent(options);

  return (
    <UniformContext result={result}>
      <UniformResolvedComposition
        result={result}
        resolveComponent={resolveRegionComponent}
        resolveEmptyPlaceholder={resolveEmptyPlaceholder}
      />
    </UniformContext>
  );
}
```

The composition renders one time. `Root` replaces its root component:

1. The SDK renders the slots of the root, and gives them to `Root` as the `slots` prop.
2. `Root` puts the slots in the store.
3. `Root` returns the page JSX. Each `UniformRegion` in 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`

```tsx
import { UniformSlot } from "@uniformdev/next-app-router/component";
import { getRegionSlots } from "@/lib/uniform/regions";

type UniformRegionProps = {
  // The public ID of a slot on the page composition.
  name: string;
  className?: string;
};

export function UniformRegion({ name, className }: UniformRegionProps) {
  const slot = getRegionSlots()?.[name];
  return <div className={className}>{slot ? <UniformSlot slot={slot} /> : null}</div>;
}
```

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

```ts
import { requireComposition, resolveRouteFromPath } from "@uniformdev/next-app-router";
import { getPageState, type SearchParams } from "@/lib/uniform/getPageState";

export async function resolvePageRoute(path: string, searchParams: SearchParams) {
  const pageState = await getPageState(path, searchParams);
  const result = await resolveRouteFromPath({ path: pageState.routePath, pageState });

  // Apply Uniform redirects. Keep the page when Uniform has no composition.
  if (result.route?.type === "redirect") {
    requireComposition(result);
  }

  return result;
}
```

`getPageState` makes the page state for the request:

- **Published:** it does not read `searchParams`, so the route stays static. It sets `edgeMode: true` for the edge middleware.
- **Draft:** it reads `searchParams` to 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`

```tsx
import { UniformRegion } from "@/components/uniform/UniformRegion";
import { UniformRegions } from "@/components/uniform/UniformRegions";
import type { SearchParams } from "@/lib/uniform/getPageState";
import { resolvePageRoute } from "@/lib/uniform/resolvePageRoute";

type Props = { params: Promise<{ slug: string }>; searchParams: SearchParams };

// Render each page on its first visit, then serve it from the cache.
export const generateStaticParams = async () => [];

export default async function ProductPage({ params, searchParams }: Props) {
  const { slug } = await params;
  const result = await resolvePageRoute(`/products/${slug}`, searchParams);

  return (
    <UniformRegions result={result}>
      <header>Site header</header>
      <main>
        <h1>Product {slug}</h1>
        <UniformRegion name="hero" />
        <p>Product details that Next.js owns.</p>
        <UniformRegion name="related" />
      </main>
      <footer>Site footer</footer>
    </UniformRegions>
  );
}
```

- The `name` of 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 `resolvePageRoute` must match a project map node. Here, `/products/shoes` matches the node `/products/:slug`.
- To set the page title from Uniform, refer to [SEO](https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes/seo). Call `getUniformMetadata` only for a composition result:

  `app/products/[slug]/page.tsx`

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

- `UniformRegions` replaces the root component. Thus `resolveComponent` does 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](https://docs.uniform.app/docs/sdk/nextjs-app-router/resolving-compositions#step-3-configure-the-middleware-and-the-preview).
- **Lite mode:** use `handleUniformRoute`, and remove `edgeMode: true` from `getPageState`:

  `middleware.ts`

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

```ts
import {
  createPreviewGETRouteHandler,
  createPreviewOPTIONSRouteHandler,
  createPreviewPOSTRouteHandler,
} from "@uniformdev/next-app-router/handler";

export const GET = createPreviewGETRouteHandler({
  // Canvas sends the project map path. Return the URL of your Next.js route.
  resolveFullPath: ({ path, slug }) => path ?? slug,
});
export const POST = createPreviewPOSTRouteHandler();
export const OPTIONS = createPreviewOPTIONSRouteHandler();
```

- 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](https://docs.uniform.app/docs/sdk/nextjs-app-router/preview#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`

```tsx
import {
  requireComposition,
  resolvePlaygroundRoute,
  UniformPlayground,
  type PlaygroundParameters,
} from "@uniformdev/next-app-router";
import { draftMode } from "next/headers";
import { resolveComponent } from "@/components/resolveComponent";
import { UniformRegion } from "@/components/uniform/UniformRegion";
import { UniformRegions } from "@/components/uniform/UniformRegions";

export default async function PlaygroundPage({ params }: PlaygroundParameters) {
  if (!(await draftMode()).isEnabled) {
    return <h1>Playground is only available in draft mode</h1>;
  }

  const { code } = await params;
  const result = requireComposition(await resolvePlaygroundRoute({ code }));
  const composition = result.route.compositionApiResponse.composition;

  // A composition pattern of the page type: show one region for each slot.
  if (composition.type === "page") {
    return (
      <UniformRegions result={result}>
        {Object.keys(composition.slots ?? {}).map((name) => (
          <UniformRegion key={name} name={name} />
        ))}
      </UniformRegions>
    );
  }

  // Other patterns: the default playground.
  return <UniformPlayground result={result} resolveComponent={resolveComponent} />;
}
```

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

1. The middleware rewrites the request to its own URL. Next.js renders your route.
2. The page makes the page state, and resolves the route from the path with `resolveRouteFromPath`.
3. A Uniform redirect gives a redirect. No composition gives the page with empty regions.
4. `UniformRegions` renders `UniformContext` and the composition one time. The root component puts the slots in the store, and returns the page JSX.
5. Each `UniformRegion` reads its slot from the store, and renders the slot items.
6. 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](https://docs.uniform.app/docs/sdk/nextjs-app-router/resolving-compositions#render-more-than-one-composition).
- **Server components only.** `UniformRegions` and `UniformRegion` use `serverContext`. A client component cannot read the slots. It can render a `UniformRegion` that a server component gives to it as `children`.
- **Two route calls.** `generateMetadata` and the page both resolve the route. For published content, Next.js can serve the second call from its data cache.
