# Build your first Uniform page with the Next.js App Router SDK

> Connect a new Next.js 16 app to Uniform, build a page component and a hero component, edit them in Canvas, add a personalization and turn on edge mode.

Source: https://docs.uniform.app/docs/sdk/nextjs-app-router/tutorial

In this tutorial, you build a Next.js app that renders pages from Uniform. At the end, you can:

- Render a Uniform composition at its URL.
- Edit the page in Canvas, with visual editing.
- Show a different hero to visitors from a campaign.
- Show the correct hero on the first paint, with edge mode.

The tutorial takes about one hour. If you do not know the Uniform terms, read [Core concepts](https://docs.uniform.app/docs/sdk/nextjs-app-router/concepts) first.

## What you build

```
my-uniform-app/
├── app/
│   ├── api/preview/route.ts          # Preview and webhook handler
│   ├── layout.tsx                    # Root layout (from create-next-app)
│   ├── uniform/[code]/page.tsx       # Renders each composition
│   └── playground/[code]/page.tsx    # Renders patterns in Canvas
├── components/
│   ├── hero.tsx                      # The Hero component
│   ├── page.tsx                      # The Page component
│   └── resolveComponent.tsx          # Uniform type → React component
├── middleware.ts                     # Makes the code for each request
├── uniform.server.config.ts          # Server configuration
└── next.config.ts                    # Wrapped with withUniformConfig
```

## Before you start

You must have:

- Node.js 20.9 or later
- A Uniform team, and an empty Uniform project
- An API key for the project with permission to read and write content. Refer to [API access](https://docs.uniform.app/docs/guides/api-access).

---

## Part 1: Prepare the Uniform project

Do these steps in Uniform. For the details of each screen, refer to the linked guides.

### Step 1: Define the component types

Make two component types in the component library. Refer to [Components](https://docs.uniform.app/docs/guides/models/components).

| Component type | Public ID | Composition component | Parameters | Slots |
| --- | --- | --- | --- | --- |
| Page | `page` | Yes | None | `content`: allow Hero and Personalization |
| Hero | `hero` | No | `title` (Text), `description` (Rich text) | None |

The public IDs are important. Your code uses them.

Make the Hero type first. Then the `content` slot of Page can allow it. Allow the **Personalization** system component in the `content` slot too. You add a personalization to this slot in Part 5.

### Step 2: Make a composition

1. Make a composition of the type **Page**, with the name **Home**. Refer to [Compositions](https://docs.uniform.app/docs/guides/composition/compositions).
2. Add a **Hero** to the `content` slot.
3. Set the title to "Hello World", and set a description.
4. Publish the composition.

### Step 3: Connect the composition to a URL

In the project map, attach the **Home** composition to the root node `/`. Refer to [Project maps](https://docs.uniform.app/docs/guides/project-maps).

### Step 4: Get the API values

Write down these values. You use them in Step 6.

- The project ID
- The API key
- A preview secret. This is a value that you choose, for example the output of `openssl rand -base64 32`.

---

## Part 2: Connect a Next.js app

### Step 5: Make the Next.js app and install the SDK

1. Make a Next.js app with the App Router and TypeScript:

   ```bash
   npx create-next-app@latest my-uniform-app
   cd my-uniform-app
   ```

   Select the recommended Next.js defaults. They include the App Router, TypeScript, Tailwind CSS and the `@/*` import alias. You change one of the defaults in Step 7.
2. Install the SDK:

   ```bash
   v=20.81.1-alpha.25.sha-f076f9b857
   npm install @uniformdev/next-app-router@$v @uniformdev/richtext@$v
   ```

   `@uniformdev/next-app-router` contains the server SDK, the components, the middleware, the configuration helpers and the preview handlers. `@uniformdev/richtext` gives the rich text type for the Hero.

> **Warning:**
>
> Use the same version for all `@uniformdev` packages. Different versions can install two copies of `@uniformdev/context`, and then the browser Context does not work correctly.

### Step 6: Set the environment variables

Make `.env.local` in the project root:

```bash
UNIFORM_API_KEY=your-api-key
UNIFORM_PROJECT_ID=your-project-id
UNIFORM_PREVIEW_SECRET=your-preview-secret
```

For all the variables, refer to [Set the environment variables](https://docs.uniform.app/docs/sdk/nextjs-app-router#set-the-environment-variables).

### Step 7: Wrap the Next.js configuration

`create-next-app` makes a `next.config.ts` file. Change it to this:

`next.config.ts`

```ts
import { withUniformConfig } from "@uniformdev/next-app-router/config";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // Keep the other options that create-next-app added, for example the Tailwind rule:
  turbopack: {
    rules: {
      "*.css": {
        loaders: ["@tailwindcss/turbopack"],
        as: "*.css",
      },
    },
  },
};

export default withUniformConfig(nextConfig);
```

- Wrap the configuration with `withUniformConfig`.
- Remove `cacheComponents: true` and `partialPrefetching: true`. This tutorial does not use Cache Components. With Cache Components, the route of Step 10 fails. To use Cache Components later, refer to [Caching](https://docs.uniform.app/docs/sdk/nextjs-app-router/caching#cache-components).
- Keep the other options. If you remove the `turbopack` rule, Tailwind CSS stops working.

`withUniformConfig` looks for `uniform.server.config` in the project root, with the extension `.js`, `.cjs`, `.mjs` or `.ts`. When it finds the file, it adds an alias for Turbopack, and for the webpack server build. Then the SDK reads your configuration.

### Step 8: Add the server configuration

`uniform.server.config.ts`

```ts
import type { UniformServerConfig } from "@uniformdev/next-app-router/config";

const config: UniformServerConfig = {
  defaultConsent: true,
  playgroundPath: "/playground",
};

export default config;
```

- `defaultConsent: true` lets the Uniform Context store visitor data without a consent banner. Set it to `false` if your site asks for consent.
- `playgroundPath` is the route that shows patterns in Canvas (Step 11).

Your file replaces the default configuration fully, so always set these two options. For all the options, refer to [Server configuration](https://docs.uniform.app/docs/sdk/nextjs-app-router/configuration).

### Step 9: Add the middleware

Make `middleware.ts` in the project root. The middleware writes the route and the request state into a code, and rewrites the request to `/uniform/[code]`. It makes no network calls.

`middleware.ts`

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

This is lite mode: the browser chooses the personalization variants. You change to edge mode in Part 5.

> **Keep middleware.ts on the edge runtime:**
>
> Next.js 16 renames `middleware.ts` to `proxy.ts`. When you start the app, Next.js shows 3 warnings: "The "middleware" file convention is deprecated", "You are using an experimental edge runtime" and "The Edge Runtime is deprecated". These warnings are expected. Do not rename the file, and do not run the codemod that Next.js recommends. On Vercel, the SDK cannot read draft mode in `proxy.ts` ([vercel/next.js#82344](https://github.com/vercel/next.js/issues/82344)), and Canvas preview shows published content. Keep `middleware.ts` and `runtime: "experimental-edge"`.

For all the options, refer to [Middleware configuration](https://docs.uniform.app/docs/sdk/nextjs-app-router/middleware).

### Step 10: Add the composition route

Make `app/uniform/[code]/page.tsx`. The middleware rewrites each request to this route:

`app/uniform/[code]/page.tsx`

```tsx
import { UniformComposition, type UniformPageParameters } from "@uniformdev/next-app-router";
import { resolveComponent } from "@/components/resolveComponent";

// 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} resolveComponent={resolveComponent} />;
}
```

`UniformComposition` is an async server component. It does these tasks:

- It resolves the route from the code with `resolveRouteFromCode`. To use another resolver, set `resolveRoute`, for example the `'use cache'` resolver from [Caching](https://docs.uniform.app/docs/sdk/nextjs-app-router/caching#cache-components).
- It applies the result:

  - A Uniform redirect with the status 301 or 308 calls `permanentRedirect()`, and Next.js sends a 308.
  - A redirect with a different status calls `redirect()`, and Next.js sends a 307.
  - A missing route calls `notFound()`.

  Next.js does not keep the exact status code. If the page streams already, for example below a `loading.tsx` file, Next.js redirects in the browser with a meta tag.
- It renders the composition in `UniformContext`. `UniformContext` starts the Uniform Context in the browser. In preview, it also adds the visual editing script.

Do not put `UniformContext` in `layout.tsx`. `UniformComposition` adds it for you.

| Prop | Required | Description |
| --- | --- | --- |
| `code` | Yes | The `[code]` path segment from the middleware. |
| `resolveComponent` | Yes | Your component resolver (Step 13). |
| `resolveRoute` | No | The route resolver. The default is `resolveRouteFromCode`. |
| `clientContextComponent` | No | A [custom client context](https://docs.uniform.app/docs/sdk/nextjs-app-router/client-context#custom-client-context). |
| `resolveEmptyPlaceholder` | No | The component for an empty slot in Canvas. |
| `compositionCache` | No | A [composition cache](https://docs.uniform.app/docs/sdk/nextjs-app-router/resolving-compositions#composition-cache). |

> **Warning:**
>
> With Cache Components (`cacheComponents: true`), an empty `generateStaticParams` makes the build fail. Return at least one code, for example `createUniformStaticParams({ paths: ["/"] })`. Refer to [Static generation (ISR)](https://docs.uniform.app/docs/sdk/nextjs-app-router/incremental-static-regeneration-isr).

### Step 11: Add the playground route

Canvas shows patterns on the playground route. Make `app/playground/[code]/page.tsx`:

`app/playground/[code]/page.tsx`

```tsx
import { UniformPlayground, type PlaygroundParameters } 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} resolveComponent={resolveComponent} />;
}
```

The middleware sends pattern previews to `${playgroundPath}/[code]` in draft mode. If `UniformPlayground` renders outside draft mode, it shows the message "Playground is only available in draft mode".

### Step 12: Add the preview and webhook handler

Make `app/api/preview/route.ts`:

`app/api/preview/route.ts`

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

export const GET = createPreviewGETRouteHandler();
export const POST = createPreviewPOSTRouteHandler();
export const OPTIONS = createPreviewOPTIONSRouteHandler();
```

- **`GET`** starts a preview from Canvas. It checks `UNIFORM_PREVIEW_SECRET`, enables Next.js draft mode, and redirects to the page. Refer to [Preview](https://docs.uniform.app/docs/sdk/nextjs-app-router/preview).
- **`POST`** receives Uniform webhooks, and revalidates the cache tags and paths of the changed content. Refer to [Caching](https://docs.uniform.app/docs/sdk/nextjs-app-router/caching#webhooks).
- **`OPTIONS`** answers CORS preflight requests from Canvas.

---

## Part 3: Build the components

### Step 13: Map Uniform types to React components

Make `components/resolveComponent.tsx`. The SDK calls this function for each component in the composition:

`components/resolveComponent.tsx`

```tsx
import { type ResolveComponentFunction, 
         type ResolveComponentResult } from "@uniformdev/next-app-router";
import type { ComponentProps } from "@uniformdev/next-app-router/component";

import { HeroComponent } from "./hero";
import { PageComponent } from "./page";

const componentMap: Record<string, ResolveComponentResult["component"]> = {
  page: PageComponent,
  hero: HeroComponent,
};

const NotFound = ({ type }: ComponentProps) => 
                <div>Component not found: {type}</div>;

export const resolveComponent: ResolveComponentFunction = ({ component }) => ({
  component: componentMap[component.type] ?? NotFound,
});
```

The keys of `componentMap` are the public IDs from Step 1. An unknown type renders the `NotFound` component, so a new component type in Uniform does not break the page.

### Step 14: Build the Page component

The Page component is the root of the composition. It renders its `content` slot:

`components/page.tsx`

```tsx
import { type ComponentProps,
         UniformSlot } from "@uniformdev/next-app-router/component";

type PageSlots = "content";

export const PageComponent = ({ slots }: ComponentProps<unknown, PageSlots>) => (
  <main>
    <UniformSlot slot={slots.content} />
  </main>
);
```

`UniformSlot` renders the components that authors put in the slot, in their order. For more ways to render slots, refer to [Components and slots in code](https://docs.uniform.app/docs/sdk/nextjs-app-router/components#slots).

### Step 15: Build the Hero component

`components/hero.tsx`

```tsx
import { type ComponentParameter,
         type ComponentProps,
         UniformRichText,
         UniformText } from "@uniformdev/next-app-router/component";
import type { ParameterRichTextValue } from "@uniformdev/richtext";

type HeroParameters = {
  title?: ComponentParameter<string>;
  description?: ComponentParameter<ParameterRichTextValue>;
};

export const HeroComponent = ({
  parameters: { title, description },
  component,
}: ComponentProps<HeroParameters>) => (
  <section>
    {title ? (
      <UniformText component={component}
                   parameter={title} as="h1"
                   placeholder="Enter a title" />
    ) : null}
    {description ? (
      <UniformRichText component={component}
                       parameter={description}
                       placeholder="Enter a description" />
    ) : null}
  </section>
);
```

- The parameter names are the public IDs of the parameters from Step 1.
- Make all parameters optional with `?`. A parameter can be `undefined`, for example when an author adds a component and does not fill it in.
- `UniformText` and `UniformRichText` let authors edit the text on the page in Canvas. They require a parameter object, so render them only when the parameter exists.

For all the props and helpers, refer to [Components and slots in code](https://docs.uniform.app/docs/sdk/nextjs-app-router/components).

### Step 16: Run the app

```bash
npm run dev
```

Open `http://localhost:3000`. The page shows the "Hello World" hero from Uniform.

If the page shows a 404, make sure that the composition is published, and that the project map node `/` has the composition.

---

## Part 4: Edit in Canvas

### Step 17: Set the preview URL

In the settings of your Uniform project, set the preview URL to:

```
http://localhost:3000/api/preview?secret=your-preview-secret
```

Use the same secret as `UNIFORM_PREVIEW_SECRET`. Refer to [Visual editing](https://docs.uniform.app/docs/guides/composition/visual-editing).

### Step 18: Edit the page

1. Open the **Home** composition in Canvas. The preview panel shows your app.
2. Select the title of the hero on the page, and type a new title. The page shows the change as you type.
3. Add a second Hero to the `content` slot. The page shows it.

In preview, the SDK renders draft content. You see changes before you publish them. For how preview works, refer to [Preview](https://docs.uniform.app/docs/sdk/nextjs-app-router/preview).

> **Note:**
>
> If Canvas shows published content and not your changes, make sure that the middleware file is `middleware.ts` with `runtime: "experimental-edge"` (Step 9). For more causes, refer to [Troubleshooting](https://docs.uniform.app/docs/sdk/nextjs-app-router/troubleshooting).

### Step 19: Publish

Publish the composition. Then open `http://localhost:3000` outside Canvas. The page shows the published content.

Under `npm run dev`, the SDK does not cache content, so you see the change at once. In production, a webhook revalidates the cache (Part 6).

---

## Part 5: Add a personalization

### Step 20: Make a signal

In Uniform, make a signal with the name **Launch campaign**. Set its criteria to the query string `utm_campaign` with the value `launch`. Publish the Context manifest. Refer to [Signals](https://docs.uniform.app/docs/guides/classification/signals).

### Step 21: Personalize the hero

1. In Canvas, add a personalization to the `content` slot of **Home**. Refer to [Personalization](https://docs.uniform.app/docs/guides/personalization).
2. Set the **Analytics tracking name** of the personalization, for example `home-hero`. This field is required. If it is empty, you cannot publish.
3. Put the "Hello World" hero in the personalization as the default variant. Do not set criteria on it.
4. Add a second hero with the title "Welcome, launch visitor". Set its criteria to the **Launch campaign** signal. Put this hero above the default variant. The personalization selects the first variant that matches, and the default variant matches all visitors.
5. Publish the composition.

### Step 22: Look at lite mode

Open `http://localhost:3000/?utm_campaign=launch`. The page shows "Hello World" first. After the JavaScript loads, it changes to "Welcome, launch visitor".

This is lite mode. The server renders the default variant, and the browser chooses the variant of the visitor after hydration. The change on the screen is the "flicker".

### Step 23: Turn on edge mode

Edge mode chooses the variant at the edge, before the browser gets the page.

1. Install the Vercel functions package:

   ```bash
   npm install @vercel/functions
   ```
2. Replace the middleware:

   `middleware.ts`

   ```ts
   import { vercelUniformEdgeMiddleware } from "@uniformdev/next-app-router/vercel";

   export default vercelUniformEdgeMiddleware();

   export const config = {
     matcher: [
       {
         source: "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
         // The middleware fetches the cached page with this header. Do not run it again.
         missing: [{ type: "header", key: "x-uniform-edge-origin" }],
       },
     ],
     runtime: "experimental-edge",
   };
   ```
3. Expire the edge cache when you publish:

   `app/api/preview/route.ts`

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

   export const GET = createPreviewGETRouteHandler();
   export const POST = createPreviewPOSTRouteHandler({ 
                        onRevalidateTags: expireVercelRuntimeCacheTags });
   export const OPTIONS = createPreviewOPTIONSRouteHandler();
   ```
4. Edge mode works on published pages from the cache. Build and start the app:

   ```bash
   npm run build && npm run start
   ```

   On your computer, the Vercel runtime cache is not available. The log shows "Runtime Cache unavailable in this environment. Falling back to in-memory cache." This is expected. On Vercel, the SDK uses the runtime cache.
5. Open `http://localhost:3000/?utm_campaign=launch`. The page shows "Welcome, launch visitor" on the first paint, with no change.

To look at the HTML from the server:

```bash
curl -s "http://localhost:3000/?utm_campaign=launch" | grep -o "Welcome, launch visitor"
```

For how edge mode works, refer to [Edge mode execution](https://docs.uniform.app/docs/sdk/nextjs-app-router/edge-mode).

---

## Part 6: Deploy

### Step 24: Deploy to Vercel

1. Add the environment variables of Step 6 to the Vercel project, for the Production environment. Make sure that the values do not have a space or a line break at the end. A line break in `UNIFORM_PROJECT_ID` makes all pages show a 404.
2. Deploy the app to production. Use the production URL, for example `https://your-site.vercel.app`. Vercel protects preview deployments by default, and then Canvas and the webhook cannot get to the app.
3. In Uniform, change the preview URL to `https://your-site.vercel.app/api/preview?secret=your-preview-secret`.

### Step 25: Add the webhook

In Uniform, add a webhook with the URL `https://your-site.vercel.app/api/preview?secret=your-preview-secret`.

Select these events:

- `composition.published`
- `composition.deleted`
- `entry.published`
- `entry.deleted`
- `projectmap.node.insert`, `projectmap.node.update` and `projectmap.node.delete`
- `redirect.insert`, `redirect.update` and `redirect.delete`
- `manifest.published`

When an author publishes, the webhook revalidates the pages that use the content. Refer to [Caching](https://docs.uniform.app/docs/sdk/nextjs-app-router/caching#webhooks).

### Step 26: Do a test of the deployment

1. Publish a change to the **Home** composition.
2. Open the site. The page shows the change. If it does not, open the site again: the first request can get the old page while Vercel renders the new page.
3. Open `https://your-site.vercel.app/?utm_campaign=launch`. The page shows the personalized hero on the first paint.

---

## Next steps

- Add more components and slots: [Components and slots in code](https://docs.uniform.app/docs/sdk/nextjs-app-router/components)
- Add a header, a footer, images, links and metadata: [Recipes](https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes)
- Select the setup for your app: [Choose a setup](https://docs.uniform.app/docs/sdk/nextjs-app-router/choose-a-setup)
- Test your components: [Testing](https://docs.uniform.app/docs/sdk/nextjs-app-router/testing)
- Fix problems: [Troubleshooting](https://docs.uniform.app/docs/sdk/nextjs-app-router/troubleshooting)
