Resolve compositions on a page

Developer preview

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

Developer Preview

The resolvers on this page are new or changed in the developer preview of the SDK, version 20.81.1-alpha.25.sha-f076f9b857. They replace the data client (DataClient, DefaultDataClient) and precomputeComposition of the stable SDK.

Task: get a composition on the page yourself, and give it to the renderer.

UniformComposition resolves and renders one composition for the [code] route. Use this recipe when you must do one of these tasks:

  • Render more than one composition on a page, for example a page and a shared footer.
  • Change the composition data before render.
  • Get a composition by its ID or slug, without the project map.
  • Render a composition in an ordinary Next.js route, without [code].
PartJob
A resolverGets the route or composition from Uniform. It returns a ResolvedRouteResult.
requireCompositionApplies a redirect or a 404 from the result.
UniformContextStarts the Uniform Context in the browser, and adds the visual editing script and the edge state script. Render it one time on each page.
UniformResolvedCompositionRenders the components of one result.

UniformComposition is these four parts together:

// What UniformComposition does const result = requireComposition(await resolveRouteFromCode({ code })); return ( <UniformContext result={result} clientContextComponent={clientContextComponent}> <UniformResolvedComposition result={result} resolveComponent={resolveComponent} /> </UniformContext> );

All resolvers are exported from @uniformdev/next-app-router. @uniformdev/next-app-router/cache exports resolveRouteFromCode, resolveRouteFromPath, resolveCompositionById and resolveCompositionBySlug with 'use cache' for published content. Refer to Caching.

ResolverInputUsesRedirects
resolveRouteFromCode{ code }Route API, with the route path in the codeYes
resolveRouteFromPath{ path, code } or { path, pageState }Route API, with the path that you giveYes
resolveCompositionById{ compositionId, code } or { compositionId, pageState }Composition APINo
resolveCompositionBySlug{ slug, code } or { slug, pageState }Composition APINo
resolvePlaygroundRoute{ code }Composition API, in draft mode onlyNo

Each resolver must have a page state: the composition state, the locale, the release and the other values of the request. Give the code from the [code] route, or a PageState object. deserializePageState({ code }) changes a code into a PageState.

type ResolvedRouteResult = | { pageState: PageState; route: RouteGetResponseEdgehancedComposition } // route.type === "composition" | { pageState: PageState; route: RouteGetResponseRedirect } // route.type === "redirect" | { pageState: PageState; route: undefined }; // not found
  • A missing route or composition gives route: undefined.
  • An API error also gives route: undefined. The SDK writes the error to the console as a warning.
  • In the editor state, the resolver tries the editor state first, and then the draft state. pageState.compositionState tells you which state it found.
  • In the published state, the Route API applies Uniform redirects. In other states, it ignores them.

requireComposition(result) returns a result that always has a composition:

  • Redirect: it calls permanentRedirect() for a 301 or 308 redirect, and redirect() for other status codes. Next.js sends a 308 or a 307 redirect.
  • Not found: it calls notFound().

resolveRedirectHref({ route, routePath }) gives the target URL of a redirect result, if your code handles the redirects.

UniformResolvedComposition renders nothing for a redirect or a not-found result. Thus you can also skip requireComposition, and render the page without Uniform content when there is no composition.

Render UniformContext one time, and put one UniformResolvedComposition in it for each composition:

app/uniform/[code]/page.tsx

import { requireComposition, resolveCompositionBySlug, resolveRouteFromCode, UniformContext, UniformResolvedComposition, type UniformPageParameters, } from "@uniformdev/next-app-router"; import { resolveComponent } from "@/components/resolveComponent"; export default async function UniformPage(props: UniformPageParameters) { const { code } = await props.params; const [page, footer] = await Promise.all([ resolveRouteFromCode({ code }).then(requireComposition), resolveCompositionBySlug({ slug: "global-footer", code }), ]); return ( <UniformContext result={page}> <UniformResolvedComposition result={page} resolveComponent={resolveComponent} /> <UniformResolvedComposition result={footer} resolveComponent={resolveComponent} /> </UniformContext> ); }

UniformContext uses the page state and the composition data of result for the browser Context. Give it the main composition of the page.

warning

Do not render UniformContext more than one time on a page. Do not put UniformComposition or UniformPlayground in a layout that renders its own UniformContext.

Resolve the composition, change it, and then render it. This replaces the enhanceRoute method of the data client in the stable SDK:

app/uniform/[code]/page.tsx

import { enhance, EnhancerBuilder } from "@uniformdev/canvas"; import { requireComposition, resolveRouteFromCode, UniformContext, UniformResolvedComposition, type UniformPageParameters, } from "@uniformdev/next-app-router"; import { resolveComponent } from "@/components/resolveComponent"; export default async function UniformPage(props: UniformPageParameters) { const { code } = await props.params; const result = requireComposition(await resolveRouteFromCode({ code })); await enhance({ composition: result.route.compositionApiResponse.composition, enhancers: new EnhancerBuilder(), context: {}, }); return ( <UniformContext result={result}> <UniformResolvedComposition result={result} resolveComponent={resolveComponent} /> </UniformContext> ); }

The middleware makes no API calls, so you change the data only on the page.

You can render Uniform content in ordinary Next.js routes, for example app/[locale]/[slug]/page.tsx, and not in app/uniform/[code]. This is useful when an app has its pages in Next.js already, and Uniform manages only some regions of them.

The page must then make its own page state, because no code comes from the middleware.

lib/uniform/getPageState.ts

import { CANVAS_DRAFT_STATE, CANVAS_EDITOR_STATE, CANVAS_PUBLISHED_STATE, IN_CONTEXT_EDITOR_QUERY_STRING_PARAM, } from "@uniformdev/canvas"; import { determinePreviewMode } from "@uniformdev/next-app-router/middleware"; import type { PageState } from "@uniformdev/next-app-router-shared"; import { draftMode } from "next/headers"; export type SearchParams = Promise<Record<string, string | string[] | undefined>>; export async function getPageState(routePath: string, searchParams: SearchParams): Promise<PageState> { const common = { routePath, keys: undefined, defaultConsent: true, locale: undefined }; // Published: do not read searchParams, so that the route stays static. if (!(await draftMode()).isEnabled) { return { ...common, compositionState: CANVAS_PUBLISHED_STATE, releaseId: undefined, previewMode: undefined, edgeMode: true, // only with the edge middleware; remove it for lite mode }; } const params = new URLSearchParams(); for (const [key, value] of Object.entries(await searchParams)) { for (const item of [value ?? []].flat()) params.append(key, item); } return { ...common, compositionState: params.has(IN_CONTEXT_EDITOR_QUERY_STRING_PARAM) ? CANVAS_EDITOR_STATE : CANVAS_DRAFT_STATE, releaseId: params.get("releaseId") ?? undefined, previewMode: determinePreviewMode({ searchParams: params, isDraftModeEnabled: true }), }; }

Set defaultConsent to the same value as your server configuration.

app/[slug]/page.tsx

import { resolveRouteFromPath, UniformContext, UniformResolvedComposition, } from "@uniformdev/next-app-router"; import { resolveComponent } from "@/components/resolveComponent"; import { getPageState, type SearchParams } from "@/lib/uniform/getPageState"; type Props = { params: Promise<{ slug: string }>; searchParams: SearchParams }; // Render each page on its first visit, then serve it from the cache. // With Cache Components, return at least one value. export const generateStaticParams = async () => []; export default async function Page({ params, searchParams }: Props) { const { slug } = await params; const pageState = await getPageState(`/${slug}`, searchParams); const result = await resolveRouteFromPath({ path: pageState.routePath, pageState }); return ( <UniformContext result={result}> <h1>A page that Next.js owns</h1> <UniformResolvedComposition result={result} resolveComponent={resolveComponent} /> </UniformContext> ); }

Uniform can have no composition for the path. Then UniformResolvedComposition renders nothing, and the Next.js part of the page still renders. To send a 404 or a Uniform redirect, call requireComposition(result).

  • Middleware: rewrite each request to its own URL, so that Next.js uses your route:

    middleware.ts

    import { handleUniformEdgeRoute } from "@uniformdev/next-app-router/edge"; import { vercelManifestProvider } from "@uniformdev/next-app-router/vercel"; import type { NextFetchEvent, NextRequest } from "next/server"; // Make the provider one time, so that it keeps the manifest between requests. const manifest = vercelManifestProvider(); export default function middleware(request: NextRequest, event: NextFetchEvent) { return handleUniformEdgeRoute({ request, waitUntil: (promise) => event.waitUntil(promise), manifest, rewriteDestinationPath: async ({ source }) => source === "route" ? `${request.nextUrl.pathname}${request.nextUrl.search}` : "", // an empty string keeps the default playground rewrite }); } export const config = { matcher: [ { source: "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", missing: [{ type: "header", key: "x-uniform-edge-origin" }], }, ], runtime: "experimental-edge", };

    The edge middleware fetches your route and keeps the variants of the visitor. Without a filter, it processes all published pages. This agrees with edgeMode: true in getPageState. Do not add a filter, and do not use vercelUniformEdgeMiddleware. A page that the filter skips gets no transform, so its HTML shows all variants until hydration. For lite mode, use handleUniformRoute from @uniformdev/next-app-router/middleware with the same rewriteDestinationPath, and remove edgeMode from getPageState.

  • Preview: if your URLs are different from the project map paths, change them in resolveFullPath. Refer to Change the preview path.

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

The composition cache gives components the full ComponentInstance data of the composition. For example, a navigation component can read the parameters of its child components:

lib/compositionCache.ts

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

Give it to UniformComposition or UniformResolvedComposition:

<UniformComposition code={code} resolveComponent={resolveComponent} compositionCache={compositionCache} />

Then read the data in a component:

components/navigation.tsx

import type { ComponentProps } from "@uniformdev/next-app-router/component"; import { compositionCache } from "@/lib/compositionCache"; export const Navigation = ({ slots, context }: ComponentProps<unknown, "links">) => ( <nav> {slots.links.items.map((item) => { if (!item) return null; const link = compositionCache.getUniformComponent({ compositionId: context._id, componentId: item._id }); return ( <a key={item._id} href={String(link?.parameters?.url?.value ?? "#")}> {String(link?.parameters?.title?.value ?? "")} </a> ); })} </nav> );
MethodDescription
getUniformComposition({ id })Returns the root component of a composition.
getUniformComponent({ compositionId, componentId })Returns one component of a composition.
setUniformComposition(composition)Stores a composition. The renderer calls it.

Known issue in 20.81.1-alpha.25

In this version, all requests share one store, and the store uses only the composition ID as the key. A request can read the version of a composition that a different request stored, for example a draft or a different locale. The store is never cleared. Do not use the composition cache on a site where draft, locale or release versions of the same composition can render at the same time.

serverContext keeps a value for one server request. Use it to give data to components deep in the tree without props:

lib/requestLocale.ts

import { serverContext } from "@uniformdev/next-app-router"; export const [getRequestLocale, setRequestLocale] = serverContext<string>("en");

Set the value on the page before the components render. Read it in any server component of the same request. serverContext uses React cache, so it works only in server components.