Tutorial

Developer preview

This feature is in developer preview. Use with caution as it may change unexpectedly. For more information, contact us.

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

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

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.

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

Make two component types in the component library. Refer to Components.

Component typePublic IDComposition componentParametersSlots
PagepageYesNonecontent: allow Hero and Personalization
HeroheroNotitle (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.

  1. Make a composition of the type Page, with the name Home. Refer to Compositions.
  2. Add a Hero to the content slot.
  3. Set the title to "Hello World", and set a description.
  4. Publish the composition.

In the project map, attach the Home composition to the root node /. Refer to Project maps.

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.

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

    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:

    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.

Make .env.local in the project root:

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.

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

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

uniform.server.config.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.

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

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

For all the options, refer to Middleware configuration.

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

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

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

PropRequiredDescription
codeYesThe [code] path segment from the middleware.
resolveComponentYesYour component resolver (Step 13).
resolveRouteNoThe route resolver. The default is resolveRouteFromCode.
clientContextComponentNoA custom client context.
resolveEmptyPlaceholderNoThe component for an empty slot in Canvas.
compositionCacheNoA 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).

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

app/playground/[code]/page.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".

Make app/api/preview/route.ts:

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();
  • GET starts a preview from Canvas. It checks UNIFORM_PREVIEW_SECRET, enables Next.js draft mode, and redirects to the page. Refer to Preview.
  • POST receives Uniform webhooks, and revalidates the cache tags and paths of the changed content. Refer to Caching.
  • OPTIONS answers CORS preflight requests from Canvas.

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

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

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

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

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

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.


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.

  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.

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.

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


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.

  1. In Canvas, add a personalization to the content slot of Home. Refer to 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.

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

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

  1. Install the Vercel functions package:

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

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

    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:

    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:

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

For how edge mode works, refer to Edge mode execution.


  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.

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.

  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.