diff --git a/rules/app-router-sdk-v2-migration.mdc b/rules/app-router-sdk-v2-migration.mdc new file mode 100644 index 0000000..914024a --- /dev/null +++ b/rules/app-router-sdk-v2-migration.mdc @@ -0,0 +1,353 @@ +--- +alwaysApply: true +--- +# Next.js app router SDK v2 migration guide + +### Install Packages + +Install the following packages with npm: `npm i @uniformdev/next-app-router --save` + +```jsx + "@uniformdev/next-app-router": "20.48.0", +``` + +It’s best to reference all three in your `package.json` to avoid version mismatches as new packages are released. + +### New Middleware + +Middleware is now required to route requests and evaluate compositions, the basic structure should look like this: + +```jsx +import { uniformMiddleware } from '@uniformdev/next-app-router/middleware'; + +export default uniformMiddleware(); + +// IMPORTANT: runtime: "experimental-edge" is required for the middleware to work correctly for preview in Next.js 16 +export const config = { + matcher: [ + "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", + ], + runtime: "experimental-edge", +}; + +``` + +(Optionally) You can also rewrite where the middleware will redirect to. This can be useful if you are trying to code split your components and do not want to define them all on every page. + +```jsx +import { handleUniformRoute } from "@uniformdev/next-app-router/middleware"; +import { NextRequest } from "next/server"; + +export default (request: NextRequest) => { + return handleUniformRoute({ + request, + rewrite: async ({ code, route }) => { + if (route.compositionApiResponse.composition.type === "page") { + return `/rewrite-here-instead/${code}`; + } + + return `/default-rewrite/${code}`; + }, + }); +}; + +``` + +### Server config is optional now + +If you have a `uniform.server.config.ts` or `uniform.server.config.js` in the application root, you can remove it if it matches the following default: + +```ts +import { UniformServerConfig } from "@uniformdev/next-app-router/config"; + +const config: UniformServerConfig = { + defaultConsent: true, + quirkSerialization: true, + middlewareRuntimeCache: true, + playgroundPath: "/playground", +}; + +export default config; +``` + +### New File Structure + +Before the expectation was that you could have a catch all page route, now by default we expect you to have the following file structure: + +- app + - uniform + - [code] + - page.tsx + +The default implementation of `page.tsx` should be: + +```jsx +import { + UniformComposition, + UniformPageParameters, + createUniformStaticParams, + resolveRouteFromCode, +} from "@uniformdev/next-app-router"; +import { CustomUniformClientContext } from "@/components/CustomUniformClientContext"; + +import { resolveComponent } from "../../../components/resolveComponent"; + +export const generateStaticParams = async () => { + return createUniformStaticParams({ + // paths: ["/"], + // Important: for localized sites, you need to add the locales to the paths + paths: ["/en"], + }); +}; + +export default async function UniformPage(props: UniformPageParameters) { + const { code } = await props.params; + return ( + + ); +} + +``` + +## New Playground implementation + +- app + - playground + - [code] + - page.tsx + +The default implementation of `page.tsx` should be: + +```jsx +import { + createUniformPlaygroundStaticParams, + PlaygroundParameters, + resolvePlaygroundRoute, + UniformPlayground, +} from "@uniformdev/next-app-router"; + +import { resolveComponent } from "@/components/resolveComponent"; + +export const generateStaticParams = async () => { + return createUniformPlaygroundStaticParams({ + // Important: for localized sites, you need to add the locales to the paths + paths: ["/en"], + }); +}; + +export default async function PlaygroundPage({ params }: PlaygroundParameters) { + const { code } = await params; + return ( + + ); +} +``` + +### ComponentProps + +This type was reworked a little, the older interface used to look like this: + +```jsx +export type ComponentProps = TProps & { + component: ComponentInstance; + context: CompositionContext; + slots: Record; + slotName: string | undefined; + slotIndex: number | undefined; +}; +``` + +Now it looks like this: + +``` +export type ComponentProps< + TParameters extends Record | unknown = Record, + TSlotNames extends string = string, +> = { + type: string; + variant: string | null; + slots: Record; + parameters: TParameters; + component: ComponentContext; + context: CompositionContext; +}; +``` + +The following changes were made: + +- You are no longer passed the entire component +- Parameters are no longer added to the top level of the object +- Parameters are now of type `Record` + +```jsx +type HeroParamters = { + title: string +} + +type HeroProps = ComponentProps + +const Hero = ({ title }: HeroProps) => { + return ( +

{title}

+ ) +} +``` + +Should now be + +```jsx +type HeroParamters = { + title: ComponentParameter +} + +type HeroProps = ComponentProps + +const Hero = ({ parameters: { title }}: HeroProps) => { + return ( +

{title.value}

+ ) +} +``` + +This change was made to keep the full parameter intact and hopefully be able to use this data to implement conditional values. + +### UniformSlot + +The slot definition type used to look like this: + +```jsx +export type SlotDefinition = { + name: string; + items: PropsWithChildren['children'][]; +}; +``` + +Now it looks like this: + +```jsx +export type SlotDefinition = { + name: string; + items: ({ + _id: string; + [CANVAS_PERSONALIZATION_PARAM]: VariantMatchCriteria | undefined; + component: ReactNode; + } | null)[]; +}; +``` + +Slots can be rendered with a single prop: + +```jsx + +``` + +If you would like to extract all of the components that make up a slot, you can do the following: + +```jsx +const headerItems = slots.header.items.map((item) => item?.component); +``` + +If you would like to resolve the ComponentInstance data for the slot, you can use a component cache to resolve these. Currently this requires manual configuration as it wasnt working correctly when exported from the package: + +Define your cache, in a file like `lib/cache.ts` + +```jsx +import { createCompositionCache } from "@uniformdev/next-app-router"; + +export const compositionCache = createCompositionCache(); +``` + +Pass this cache to `UniformComposition` as a prop: + +```jsx + +``` + +Then in your components, you can resolve component data including components that make up a slot: + +```jsx +import { + ComponentProps, + UniformSlot, +} from "@uniformdev/next-app-router/component"; + +import { QuirkButton } from "./custom/QuirkButton"; +import { compositionCache } from "@/lib/cache"; + +export type PageProps = unknown; +export type PageSlots = "content" | "header" | "footer"; + +export const Page = ({ + slots, + context, +}: ComponentProps) => { + const headerItems = slots.header.items.map((item) => { + const resolved = compositionCache.getUniformComponent({ + componentId: item!._id, + compositionId: context._id, + }); + + return resolved; + }); + + return ( +
This is a page.
+ ); +}; + +``` + +### Adapter Components + +An adapter layer also exists to easy the transition from V1 to V2, use the built in `createAdapterResolveComponentFunction` function and pass the component mappings for your site. + +```jsx +import { createAdapterResolveComponentFunction } from '@uniformdev/next-app-router/compat'; + +import * as mappings from './mappings'; + +export const resolveComponent = createAdapterResolveComponentFunction({ mappings }); + +``` + +Adjust your mappings to use `ResolveComponentResultWithType` and adding `mode: 'adapted'` to each definition that you are adapting. + +```tsx +import type { + ComponentProps, + ResolveComponentResultWithType, +} from '@uniformdev/next-app-router/compat'; + +type PageComponentParameters = { + text: string; +}; + +const PageComponent = ({ text }: ComponentProps) => { + return
{text}
; +}; + +export const pageMapping: ResolveComponentResultWithType = { + type: 'page', + component: PageComponent, + mode: 'adapted', +}; +``` + +Use `UniformText` and `UniformSlot` components from the same `compat` export. + +### Clients + +- `getDefaultRouteClient` is now `getRouteClient` diff --git a/rules/optional-personal-preference/solution-architecture.mdc b/rules/optional-personal-preference/solution-architecture.mdc index d35d08d..b832273 100644 --- a/rules/optional-personal-preference/solution-architecture.mdc +++ b/rules/optional-personal-preference/solution-architecture.mdc @@ -9,7 +9,6 @@ alwaysApply: false - Always generate clean code and remove unused code, so the linter and typescript checks pass automatically. IMPORTANT: do not ever run build of the application as a part of the validation as this breaks developer server and is causing issues. - Always use `pnpm` to install packages if you see `pnpm-lock.yaml` in the root folder, otherwise use `npm`. Never use `yarn`. - - You are a senior React engineer, follow best practices of React at all times. - Use TailwindCSS best practices when building CSS. Don't re-invent the wheel. - You are a senior engineer, do not try to impress me with over-delivery. diff --git a/rules/uniform-next-app-router.mdc b/rules/uniform-next-app-router.mdc index 5ee5bb2..3630c72 100644 --- a/rules/uniform-next-app-router.mdc +++ b/rules/uniform-next-app-router.mdc @@ -3,332 +3,595 @@ description: Rules for Uniform SDK for Next.js App Router globs: alwaysApply: true --- +# Uniform React Next.js App Router SDK Developer Reference -# Uniform React Next App Router Developer Reference - -This document details how to use Uniform with React.js with Next App Router. +This document details how to use Uniform SDK with React.js and Next.js App Router. @uniform.mdc describes general Uniform principles and practices. @uniform-sdk.mdc describes framework-agnostic developer principles. -### Required npm packages +## Required npm packages (version 20.58.0 or later) + +```json +{ + "@uniformdev/next-app-router": "20.58.0", + "@uniformdev/context": "20.58.0", +} +``` + +## File Structure + +Required file structure: -The following npm packages must be installed to wire Uniform to Next App Router: +``` +app/ +├── api/ +│ └── preview/ +│ └── route.ts # Preview handler +├── layout.tsx # Root layout +├── uniform/ +│ └── [code]/ +│ └── page.tsx # Main composition route +└── playground/ + └── [code]/ + └── page.tsx # Playground route +middleware.ts # Required for edge personalization +uniform.server.config.ts # Server configuration +next.config.ts # Next.js config with withUniformConfig +``` -@uniformdev/canvas-next-rsc -@uniformdev/canvas +## Server Configuration -### Uniform server config +### uniform.server.config.ts is optional now: -A file names `uniform.server.config.js` should exist in the root of the Next.js project with the following content: +```ts +import { UniformServerConfig } from "@uniformdev/next-app-router/config"; -```js -/** @type {import('@uniformdev/canvas-next-rsc/config').UniformServerConfig} */ -module.exports = { +const config: UniformServerConfig = { defaultConsent: true, - evaluation: { - personalization: "hybrid", + playgroundPath: "/playground" +}; + +export default config; +``` + +Full configuration options: + +```ts +const config: UniformServerConfig = { + // Default storage consent for new visitors + defaultConsent: true, + + // Path to playground page + playgroundPath: "/playground", + + // ETag support for caching + eTags: { + generateETags: true, + cacheControl: "no-cache, must-revalidate", }, - experimental: { - quirkSerialization: true, + + // Context options + context: { + disableDevTools: false, }, + + // Quirk and middleware options + quirkSerialization: true, + middlewareRuntimeCache: true, + disableSwrMiddlewareCache: false, }; ``` -### Enable Uniform server config +### next.config.ts + +```ts +import { withUniformConfig } from "@uniformdev/next-app-router/config"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + // your Next.js config options +}; + +export default withUniformConfig(nextConfig); +``` + +## Middleware (Required) + +Middleware in `middleware.ts` at project root is required. + +**IMPORTANT for Next.js 16:** To support Uniform preview in Canvas: +1. The middleware file **MUST** be named `middleware.ts` (not `proxy.ts` or other names) +2. **MUST** export the runtime value: + +```ts +export const runtime = 'experimental-edge'; +``` + +### Basic middleware + +```ts +import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; + +export default uniformMiddleware(); + +// Required for Next.js 16 preview support +export const runtime = 'experimental-edge'; +``` + +### Middleware with locale handling + +```ts +import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; + +const defaultLocale = "en"; +const locales = ["en"]; -To make the configuration available in Next.js, modify the next.config.js file in the root of the project and add in `withUniformConfig`: +export default uniformMiddleware({ + rewriteRequestPath: async ({ url }) => ({ + path: formatPath(url.pathname, defaultLocale), + }), +}); -```js -const { withUniformConfig } = require("@uniformdev/canvas-next-rsc/config"); +const formatPath = (path: string, locale?: string | null): string => { + if (!locale) return path; + if (isLocaleInPath(path)) return path; + return `/${locale}${path}`; +}; -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, +const isLocaleInPath = (path: string): boolean => { + const [firstSegment] = path.split("/").filter(Boolean); + return firstSegment + ? (locales as string[]).some((locale) => locale === firstSegment) + : false; }; -module.exports = withUniformConfig(nextConfig); +// Required for Next.js 16 preview support +export const runtime = 'experimental-edge'; ``` -### Fetching and rendering the composition +### Full middleware options + +```ts +import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; + +export default uniformMiddleware({ + // Rewrite request path before route resolution + rewriteRequestPath: async ({ url, request }) => ({ + path: url.pathname, + keys: { locale: "en" }, // Custom keys passed to composition + }), + + // Rewrite destination after evaluation + rewriteDestinationPath: async ({ code, pageState, source }) => { + return `/uniform/${code}`; + }, + + // Paths that should precompute personalizations + pathPatternsWithVariations: ["/products/*"], + + // Content release + release: { id: "release-123" }, + + // Custom quirks to inject + quirks: { customQuirk: "value" }, + + // Override default consent + defaultConsent: true, +}); + +// Required for Next.js 16 preview support +export const runtime = 'experimental-edge'; +``` + +## Main Composition Route -In your dynamic route file (`app/[[...path]]/page.{tsx|jsx}`), it is necessary to fetch the Uniform composition instance for the current route. +`app/uniform/[code]/page.tsx`: ```tsx import { + resolveRouteFromCode, UniformComposition, - PageParameters, - retrieveRoute, -} from "@uniformdev/canvas-next-rsc"; -import { resolveComponent } from "@/uniform/resolve"; + UniformPageParameters, + createUniformStaticParams, +} from "@uniformdev/next-app-router"; +import { resolveComponent } from "@/components/resolveComponent"; + +// Enable ISR (Incremental Static Regeneration) +export const generateStaticParams = async () => { + return createUniformStaticParams({ + // paths: ["/"], + // Important: for localized sites, you need to add the locales to the paths + paths: ["/en"], + }); +}; -export default async function Page(props: PageParameters) { - const route = await retrieveRoute(props); +export default async function UniformPage(props: UniformPageParameters) { + const { code } = await props.params; return ( - <> - - + ); } ``` -### Wrapping page in Uniform Context +**Key points:** +- `UniformContext` is self-closing and placed in page.tsx (not layout.tsx) +- `UniformContext` must be wrapped in `Suspense` +- Spread `{...result}` to `UniformComposition` + +## Playground Route -In order for personalization and A/B testing functionality to work client side, we must wrap the entire page in the UniformContext component. This should modify `app/layout.tsx` and wrap `{children}`. +`app/playground/[code]/page.tsx`: ```tsx -import { UniformContext } from "@uniformdev/canvas-next-rsc"; +import { + createUniformPlaygroundStaticParams, + PlaygroundParameters, + resolvePlaygroundRoute, + UniformPlayground, +} from "@uniformdev/next-app-router"; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +import { resolveComponent } from "@/components/resolveComponent"; + +export const generateStaticParams = async () => { + return createUniformPlaygroundStaticParams({ + paths: ["/en"], + }); +}; + +export default async function PlaygroundPage({ params }: PlaygroundParameters) { + const { code } = await params; return ( - - -
- {children} -
- - + ); } ``` -### Rendering Uniform Components using React Components +## Root Layout -The `UniformComposition` component needs to know how to map a Uniform Component instance's `type` to a React component that implements the UI for that component. This is done using resolveComponent. To use the component registry, first create a component: +`app/layout.tsx` - does NOT include UniformContext: ```tsx -export const HeaderComponent = () => { - return <>Header; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "My App", + description: "My description", }; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} ``` -Then register it in the resolveComponent function: +## Component Resolution -```tsx +`components/resolveComponent.ts`: + +```ts import { - DefaultNotImplementedComponent, ResolveComponentFunction, - ResolveComponentResult, -} from "@uniformdev/canvas-next-rsc/component"; -import { HeaderComponent } from "@/components/header"; + type ResolveComponentResult, +} from "@uniformdev/next-app-router"; + +import { DefaultNotFoundComponent } from "./default"; +import { HeroComponent } from "./hero"; +import { PageComponent } from "./page"; export const resolveComponent: ResolveComponentFunction = ({ component }) => { - let result: ResolveComponentResult = { - component: DefaultNotImplementedComponent, - }; + let result: ResolveComponentResult | undefined; - if (component.type === "header") { - result = { - component: HeaderComponent, - }; + if (component.type === "page") { + result = { component: PageComponent }; + } else if (component.type === "hero") { + result = { component: HeroComponent }; } - return result; + return result || { component: DefaultNotFoundComponent }; }; ``` -#### Mapping Uniform Components to React Components - -React components that receive Uniform Component data are passed props that correspond to the shape of the component definition they render. The `ComponentProps` type can be used to make the mapping explicit: +Default fallback component: ```tsx -import { ComponentProps } from "@uniformdev/canvas-next-rsc/component"; -import { - AssetParamValue, - LinkParamValue, - RichTextParamValue, -} from "@uniformdev/canvas"; - -type HeaderParameters = { - textParameter?: string; - richTextParameter?: RichTextParamValue; - linkParameter?: LinkParamValue; - assetParameter?: AssetParamValue; - // it is critical that all parameter props values are optional, because they can be undefined - even if 'required' on the component definition -}; +import { ComponentProps } from "@uniformdev/next-app-router/component"; -type HeaderProps = ComponentProps; - -export const HeaderComponent = ({ textParameter }: HeaderProps) => { - return ( - <> - {textParameter} - - ); +export const DefaultNotFoundComponent = ({ type }: ComponentProps) => { + return
Not Found: {type}
; }; ``` -#### Accessing parameters +## Component Props and Parameters + +**CRITICAL:** +1. Parameter types must be wrapped with `ComponentParameter` +2. Parameters are accessed via the `parameters` object +3. All parameters MUST be optional (use `?`) - they can be undefined even if marked required in definition -IMPORTANT: When accessing component parameters, never use this way of accessing parameter values: `component?.parameters?..value`. While it works, there is a better way of destructuring the parameter name on props and accessing it directly as shown in the example below: +### Component example with parameters ```tsx import { + ComponentParameter, ComponentProps, UniformText, -} from "@uniformdev/canvas-next-rsc/component"; - -import { - LinkParamValue, -} from "@uniformdev/canvas"; + UniformRichText, +} from "@uniformdev/next-app-router/component"; -type ComponentParameters = { - icon?: string; - label?: string; - link?: LinkParamValue; +export type HeroProps = { + title?: ComponentParameter; + description?: ComponentParameter; }; -export const ComponentName = ({ - link, - icon, - label, +export const HeroComponent = ({ + parameters: { title, description }, component, - context, -}: ComponentProps) => { +}: ComponentProps) => { return ( - - {icon} - + <> + + + ); }; ``` -#### Rendering child slots +### Accessing raw parameter values -If a Uniform Component definition has slots defined, the components in those slots can be rendered using the `UniformSlot` component. +When not using UniformText (e.g., for non-visible values like alt text): ```tsx -import { - ComponentProps, - UniformSlot, -} from "@uniformdev/canvas-next-rsc/component"; -import { RichTextParamValue } from "@uniformdev/canvas"; - -type HeaderParameters = { - textParameter?: string; - richTextParameter?: RichTextParamValue; - // it is critical that all parameter props values are optional, because they can be undefined - even if 'required' on the component definition +const HeroComponent = ({ parameters: { title } }: ComponentProps) => { + return

{title?.value}

; }; -type HeaderSlots = "logo" | "navigation"; +``` -type HeaderProps = ComponentProps; +## Rendering Slots -export const HeaderComponent = ({ slots, context, component }: HeaderProps) => { +```tsx +import { ComponentProps, UniformSlot } from "@uniformdev/next-app-router/component"; + +export type PageProps = unknown; +export type PageSlots = "content" | "header" | "footer"; + +export const PageComponent = ({ slots }: ComponentProps) => { return ( <> - - + + + ); }; ``` -#### Rendering parameter values +### Custom slot rendering -When rendering a `text` type parameter, using the `UniformText` component will enable authors to edit the value within the Uniform preview directly. Text parameters that do not have a visible component, such as alt text, should be rendered as their raw text value: +`UniformSlot` accepts a `children` render function: ```tsx -import { - ComponentProps, - UniformText, -} from "@uniformdev/canvas-next-rsc/component"; -import { RichTextParamValue } from "@uniformdev/canvas"; + + {({ child, _id, key, slotName, slotIndex }) => ( +
+ {child} +
+ )} +
+``` -type HeaderParameters = { - textParameter?: string; - richTextParameter?: RichTextParamValue; - // it is critical that all parameter props values are optional, because they can be undefined - even if 'required' on the component definition -}; +### getUniformSlot utility + +Alternative for extracting slot items as array: + +```tsx +import { getUniformSlot } from "@uniformdev/next-app-router/component"; + +const items = getUniformSlot({ slot: slots.content }); +// Returns ReactNode[] | undefined +``` + +### Accessing slot component data + +Use composition cache to access ComponentInstance data: + +```tsx +import { createCompositionCache } from "@uniformdev/next-app-router"; + +export const compositionCache = createCompositionCache(); +``` + +Pass `compositionCache` as a prop to `UniformComposition` (see Main Composition Route above), then in components: + +```tsx +const headerItems = slots.header.items.map((item) => { + const resolved = compositionCache.getUniformComponent({ + componentId: item!._id, + compositionId: context._id, + }); + return resolved; +}); +``` + +## Client Context and Quirks -type HeaderProps = ComponentProps; +### useUniformContext hook + +For client-side context access and updates: + +```tsx +"use client"; + +import { useUniformContext } from "@uniformdev/next-app-router/component"; +import { useEffect, useState } from "react"; + +export const QuirkButton = () => { + const { context } = useUniformContext(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (context?.quirks !== undefined) { + setIsLoading(false); + } + }, [context?.quirks]); + + const updateQuirk = async () => { + await context?.update({ + quirks: { + country: "Canada", + }, + }); + }; -export const HeaderComponent = ({ component, context }: HeaderProps) => { return ( - <> - - + ); }; ``` -For rich text parameters, the `UniformRichText` component will automatically render the rich text stored as JSON to HTML: +### useQuirks hook + +Reactive access to visitor quirk values: ```tsx -import { - ComponentProps, - UniformRichText, -} from "@uniformdev/canvas-next-rsc/component"; -import { RichTextParamValue } from "@uniformdev/canvas"; +"use client"; + +import { useQuirks } from "@uniformdev/next-app-router/component"; -type HeaderParameters = { - textParameter?: string; - richTextParameter?: RichTextParamValue; - // it is critical that all parameter props values are optional, because they can be undefined - even if 'required' on the component definition +export const LocationBanner = () => { + const quirks = useQuirks(); + + return
Current country: {quirks?.country ?? "Unknown"}
; }; +``` -type HeaderProps = ComponentProps; +### useScores hook -export const HeaderComponent = ({ component, context }: HeaderProps) => { - return ( - <> - - - ); +Reactive access to visitor score values: + +```tsx +"use client"; + +import { useScores } from "@uniformdev/next-app-router/component"; + +export const InterestIndicator = () => { + const scores = useScores(); + + return
Tech interest score: {scores?.tech ?? 0}
; }; ``` -Note: asset parameters are rendered directly from props, there is no `UniformAsset` component. +### Custom Client Context Component + +For advanced configuration with plugins: + +```tsx +"use client"; -##### Asset parameters/fields +import { ContextPlugin, enableContextDevTools } from "@uniformdev/context"; +import { useRouter } from "next/navigation"; +import { + createClientUniformContext, + useInitUniformContext, + ClientContextComponent, +} from "@uniformdev/canvas-next-rsc-client-v2"; + +export const CustomUniformClientContext: ClientContextComponent = ({ + manifest, + disableDevTools, + defaultConsent, + experimentalQuirkSerialization, + compositionMetadata, +}) => { + const router = useRouter(); + + useInitUniformContext(() => { + const plugins: ContextPlugin[] = []; + + if (!disableDevTools) { + plugins.push( + enableContextDevTools({ + onAfterMessageReceived: () => { + router.refresh(); + }, + }) + ); + } + + return createClientUniformContext({ + manifest, + plugins, + defaultConsent, + experimental_quirksEnabled: experimentalQuirkSerialization, + }); + }, compositionMetadata); + + return null; +}; +``` -1. CRITICAL: Always use `flattenValues` from `@uniformdev/canvas` for handling asset parameters, not custom utility functions. -2. CRITICAL: Destructure asset parameters directly from component props (e.g., `{ logos, component, context }`) rather than accessing through `component?.parameters?.parameterName`. -3. Use `flattenValues(assetParam)` directly - it handles both single and multiple assets automatically. +Pass to UniformContext: + +```tsx + +``` + +## Asset Parameters + +Use `flattenValues` from `@uniformdev/canvas`: ```tsx import { AssetParamValue, flattenValues } from "@uniformdev/canvas"; +import { ComponentParameter, ComponentProps } from "@uniformdev/next-app-router/component"; interface MyComponentProps { - multipleImagesAssetParam: AssetParamValue; - singleImageAssetParam: AssetParamValue; + multipleImages?: ComponentParameter; + singleImage?: ComponentParameter; } function MyComponent({ - multipleImagesAssetParam, - singleImageAssetParam, -}: MyComponentProps) { - // when multiple assets are allowed, flatten to an array - const images = flattenValues(multipleImagesAssetParam); - // when only one asset is allowed, flatten to a single object - const image = flattenValues(singleImageAssetParam, { toSingle: true }); + parameters: { multipleImages, singleImage }, +}: ComponentProps) { + // Multiple assets: flatten to array + const images = flattenValues(multipleImages); + // Single asset: flatten to single object + const image = flattenValues(singleImage, { toSingle: true }); return ( <> {images?.map((img, index) => ( - + ))} @@ -336,11 +599,9 @@ function MyComponent({ } ``` -4. Do NOT create custom utility functions like `getAssetValues` when `flattenValues` already exists and works perfectly. - -### Configuring Contextual Editing Live Preview +**CRITICAL:** Always use `flattenValues` from `@uniformdev/canvas`. Do NOT create custom utility functions. -To enable contextual editing and live preview to operate within the Uniform application, we need to register a _preview handler_ and _playground page_. The preview handler is an API endpoint that Uniform invokes when preview starts. It is responsible for mapping the composition ID under preview to a redirect to the correct frontend route to display that composition. The default handler does this using project map hierarchy. +## Preview Handler `app/api/preview/route.ts`: @@ -349,34 +610,223 @@ import { createPreviewGETRouteHandler, createPreviewPOSTRouteHandler, createPreviewOPTIONSRouteHandler, -} from "@uniformdev/canvas-next-rsc/handler"; +} from "@uniformdev/next-app-router/handler"; export const GET = createPreviewGETRouteHandler({ - playgroundPath: "/playground", resolveFullPath: ({ path }) => (path ? path : "/playground"), }); export const POST = createPreviewPOSTRouteHandler(); export const OPTIONS = createPreviewOPTIONSRouteHandler(); ``` -The preview playground is a special route used to preview Uniform Patterns (reusable chunks of a page). It should use the same resolveComponent function. The playground route includes the global page shell of the application: +## Server-Side Precomputation -`app/playground/page.tsx`: +Use `precomputeComposition` to evaluate tests and personalizations server-side: ```tsx import { - UniformPlayground, - UniformPlaygroundProps, -} from "@uniformdev/canvas-next-rsc"; -import { resolveComponent } from "@/uniform/resolve"; - -export default function PlaygroundPage(props: { - searchParams: UniformPlaygroundProps["searchParams"]; -}) { - return ; + resolveRouteFromCode, + UniformComposition, + UniformContext, + precomputeComposition, +} from "@uniformdev/next-app-router"; + +export default async function UniformPage(props: UniformPageParameters) { + const result = await resolveRouteFromCode(props); + + if (!result.route) { + notFound(); + } + + // Optional: precompute tests and personalizations server-side + await precomputeComposition(result); + + return ( + <> + + + + + + ); } ``` +Options: + +```tsx +await precomputeComposition({ + pageState: result.pageState, + route: result.route, + evaluateTests: true, // or function to filter + evaluatePersonalizations: true, // or function to filter +}); +``` + +## Server Clients + +Server-only clients for data access: + +```tsx +import { + getCanvasClient, + getManifest, + getManifestClient, + getProjectMapClient, + getRouteClient, +} from "@uniformdev/next-app-router"; +``` + +### Examples + +```tsx +// Route resolution +const routeClient = getRouteClient({ + cache: { type: "force-cache" }, +}); + +// Composition access +const canvasClient = getCanvasClient({ + cache: { type: "no-cache" }, +}); + +const composition = await canvasClient.getCompositionById({ + compositionId: "abc123", + state: CANVAS_PUBLISHED_STATE, +}); + +// Manifest access +import { CANVAS_PUBLISHED_STATE } from "@uniformdev/canvas"; + +const manifest = await getManifest({ + state: CANVAS_PUBLISHED_STATE, +}); +``` + +## Vercel Geo-IP Quirks + +Middleware automatically populates quirks from Vercel's geo-IP headers: + +| Header | Quirk Key | +|--------|-----------| +| `x-vercel-ip-country` | `vc-country` | +| `x-vercel-ip-country-region` | `vc-region` | +| `x-vercel-ip-city` | `vc-city` | + +Available for personalization rules without additional configuration on Vercel. + +## Type Definitions + +### ComponentProps + +```tsx +type ComponentProps< + TParameters extends Record | unknown, + TSlotNames extends string = string, +> = { + type: string; // Component type + variant: string | undefined; // Active variant ID + slots: Record; // Child slots + parameters: TParameters; // Parameter values + component: ComponentContext; // Component metadata + context: CompositionContext; // Composition context +}; +``` + +### ComponentContext + +```tsx +type ComponentContext = { + _id: string; + _parentId: string | null; + slotName: string | undefined; + slotIndex: number | undefined; +}; +``` + +### CompositionContext + +```tsx +type CompositionContext = { + _id: string; + type: string; + state: number; + isContextualEditing: boolean; + matchedRoute: string; + dynamicInputs: Record; + pageState: PageState; +}; +``` + +### SlotDefinition + +```tsx +type SlotDefinition = { + name: string; + items: ({ + _id: string; + $pzCrit: VariantMatchCriteria | undefined; // Personalization criteria + variantId: string | undefined; // For test/personalization variants + component: ReactNode; + } | null)[]; +}; +``` + +### ComponentParameter + +```tsx +type ComponentParameter = BaseComponentParameter & { + parameterId: string; + _contextualEditing?: { isEditable: boolean }; +}; +``` + +## Uniform Manifest + +**CRITICAL!** For Next.js App Router, include these commands in package.json: + +```json +{ + "scripts": { + "uniform:push": "uniform sync push", + "uniform:publish": "uniform context manifest publish" + } +} +``` + +- `uniform:publish` is **REQUIRED** for App Router +- Run after pushing content or component definition changes + +## Quick Reference: Import Paths + +| Category | Export | Import Path | +|----------|--------|-------------| +| **Core** | `UniformComposition`, `UniformContext`, `UniformPlayground` | `@uniformdev/next-app-router` | +| **Core** | `resolveRouteFromCode`, `resolvePlaygroundRoute`, `precomputeComposition` | `@uniformdev/next-app-router` | +| **Core** | `createUniformStaticParams`, `createCompositionCache` | `@uniformdev/next-app-router` | +| **Core** | `ResolveComponentFunction`, `ResolveComponentResult` | `@uniformdev/next-app-router` | +| **Clients** | `getCanvasClient`, `getRouteClient`, `getManifest`, `getManifestClient`, `getProjectMapClient` | `@uniformdev/next-app-router` | +| **Components** | `UniformSlot`, `UniformText`, `UniformRichText`, `getUniformSlot` | `@uniformdev/next-app-router/component` | +| **Types** | `ComponentProps`, `ComponentParameter`, `ComponentContext` | `@uniformdev/next-app-router/component` | +| **Hooks** | `useUniformContext`, `useQuirks`, `useScores` | `@uniformdev/next-app-router/component` | +| **Middleware** | `uniformMiddleware`, `handleUniformRoute` | `@uniformdev/next-app-router/middleware` | +| **Config** | `withUniformConfig`, `UniformServerConfig` | `@uniformdev/next-app-router/config` | +| **Handlers** | `createPreviewGETRouteHandler`, `createPreviewPOSTRouteHandler`, `createPreviewOPTIONSRouteHandler` | `@uniformdev/next-app-router/handler` | +| **Client** | `createClientUniformContext`, `useInitUniformContext`, `ClientContextComponent` | `@uniformdev/canvas-next-rsc-client-v2` | +| **Canvas** | `flattenValues`, `AssetParamValue`, `LinkParamValue`, `RichTextParamValue` | `@uniformdev/canvas` | + +## Best Practices + +1. **Always use TypeScript** - SDK is TypeScript-first +2. **Enable edge middleware** - Set `middlewareRuntimeCache: true` for best performance +3. **Use ISR** - Configure `generateStaticParams()` for optimal performance +4. **Type all parameters** - Use `ComponentParameter` wrappers +5. **Mark parameters optional** - Always use `?` for parameter types +6. **Client components sparingly** - Use `"use client"` only when necessary (quirks, interactivity) +7. **UniformContext in page.tsx** - Wrapped in Suspense, with result prop +8. **Use reactive hooks** - `useQuirks()` and `useScores()` for client-side personalization state +9. **Leverage Vercel geo-IP** - Free personalization by location on Vercel deployments + ## Uniform manifest usage 1. CRITICAL! If using Next.js App Router, you must ignore adding the following commands specific to Uniform manifest. This is for Next.js page router only.