From 71bd594e750c4881552250ab222dedb98991074e Mon Sep 17 00:00:00 2001 From: Alex Shyba Date: Tue, 16 Dec 2025 20:40:18 -0800 Subject: [PATCH 1/6] Revise Uniform SDK documentation for Next.js App Router Updated the Uniform SDK documentation for Next.js App Router, including new sections on server configuration, middleware, component rendering, and best practices. --- rules/uniform-next-app-router.mdc | 889 +++++++++++++++++++++++------- 1 file changed, 678 insertions(+), 211 deletions(-) diff --git a/rules/uniform-next-app-router.mdc b/rules/uniform-next-app-router.mdc index 5ee5bb2..0a77f65 100644 --- a/rules/uniform-next-app-router.mdc +++ b/rules/uniform-next-app-router.mdc @@ -3,332 +3,612 @@ 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 + +```json +{ + "@uniformdev/canvas-next-rsc-v2": "^20.7.1", + "@uniformdev/canvas-next-rsc-client-v2": "^20.7.1", + "@uniformdev/canvas-next-rsc-shared-v2": "^20.7.1", + "@uniformdev/context": "^20.7.1" +} +``` + +## 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.ts + +```ts +import { UniformServerConfig } from "@uniformdev/canvas-next-rsc-v2/config"; + +const config: UniformServerConfig = { + defaultConsent: true, + playgroundPath: "/playground", + experimental: { + quirkSerialization: true, + middlewareRuntimeCache: true, + }, +}; -### Uniform server config +export default config; +``` -A file names `uniform.server.config.js` should exist in the root of the Next.js project with the following content: +Full configuration options: -```js -/** @type {import('@uniformdev/canvas-next-rsc/config').UniformServerConfig} */ -module.exports = { +```ts +const config: UniformServerConfig = { + // Default storage consent for new visitors defaultConsent: true, - evaluation: { - personalization: "hybrid", + + // Path to playground page + playgroundPath: "/playground", + + // Cache settings for different data types + manifestCache: { type: "force-cache" }, + canvasCache: { type: "revalidate", interval: 60 }, + projectMapCache: { type: "force-cache" }, + + // ETag support for caching + eTags: { + generateETags: true, + cacheControl: "no-cache, must-revalidate", }, + + // Context options + context: { + disableDevTools: false, + }, + + // Experimental features experimental: { quirkSerialization: true, + middlewareRuntimeCache: true, + disableSwrMiddlewareCache: false, }, }; ``` -### Enable Uniform server config +### next.config.ts + +```ts +import { withUniformConfig } from "@uniformdev/canvas-next-rsc-v2/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/canvas-next-rsc-v2/middleware"; + +export default uniformMiddleware(); + +// Required for Next.js 16 preview support +export const runtime = 'experimental-edge'; +``` + +### Middleware with locale handling -To make the configuration available in Next.js, modify the next.config.js file in the root of the project and add in `withUniformConfig`: +```ts +import { uniformMiddleware } from "@uniformdev/canvas-next-rsc-v2/middleware"; -```js -const { withUniformConfig } = require("@uniformdev/canvas-next-rsc/config"); +const defaultLocale = "en"; +const locales = ["en"]; + +export default uniformMiddleware({ + rewriteRequestPath: async ({ url }) => ({ + path: formatPath(url.pathname, defaultLocale), + }), +}); -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, +const formatPath = (path: string, locale?: string | null): string => { + if (!locale) return path; + if (isLocaleInPath(path)) return path; + return `/${locale}${path}`; }; -module.exports = withUniformConfig(nextConfig); +const isLocaleInPath = (path: string): boolean => { + const [firstSegment] = path.split("/").filter(Boolean); + return firstSegment + ? (locales as string[]).some((locale) => locale === firstSegment) + : false; +}; + +// Required for Next.js 16 preview support +export const runtime = 'experimental-edge'; +``` + +### Full middleware options + +```ts +import { uniformMiddleware } from "@uniformdev/canvas-next-rsc-v2/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'; ``` -### Fetching and rendering the composition +## 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 { Suspense } from "react"; import { + resolveRouteFromCode, UniformComposition, - PageParameters, - retrieveRoute, -} from "@uniformdev/canvas-next-rsc"; -import { resolveComponent } from "@/uniform/resolve"; + UniformPageParameters, + createUniformStaticParams, + UniformContext, +} from "@uniformdev/canvas-next-rsc-v2"; +import { notFound } from "next/navigation"; +import { resolveComponent } from "@/components/resolveComponent"; + +// Enable ISR (Incremental Static Regeneration) +export const generateStaticParams = async () => { + return createUniformStaticParams({ + paths: ["/"], + }); +}; + +export default async function UniformPage(props: UniformPageParameters) { + const result = await resolveRouteFromCode(props); + + if (!result.route) { + notFound(); + } -export default async function Page(props: PageParameters) { - const route = await retrieveRoute(props); 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` -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}`. +## Playground Route + +`app/playground/[code]/page.tsx`: ```tsx -import { UniformContext } from "@uniformdev/canvas-next-rsc"; +import { + resolvePlaygroundRoute, + UniformPageParameters, + UniformPlayground, +} from "@uniformdev/canvas-next-rsc-v2"; +import { resolveComponent } from "@/components/resolveComponent"; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default async function PlaygroundPage(props: UniformPageParameters) { + const result = await resolvePlaygroundRoute(props); 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/canvas-next-rsc-v2"; + +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 -}; - -type HeaderProps = ComponentProps; +import { ComponentProps } from "@uniformdev/canvas-next-rsc-v2/component"; -export const HeaderComponent = ({ textParameter }: HeaderProps) => { - return ( - <> - {textParameter} - - ); +export const DefaultNotFoundComponent = ({ type }: ComponentProps) => { + return
Not Found: {type}
; }; ``` -#### Accessing parameters +## Component Props and Parameters -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: +**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 + +### Component example with parameters ```tsx import { + ComponentParameter, ComponentProps, UniformText, -} from "@uniformdev/canvas-next-rsc/component"; - -import { - LinkParamValue, -} from "@uniformdev/canvas"; + UniformRichText, +} from "@uniformdev/canvas-next-rsc-v2/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/canvas-next-rsc-v2/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 -type HeaderProps = ComponentProps; +Alternative for extracting slot items as array: + +```tsx +import { getUniformSlot } from "@uniformdev/canvas-next-rsc-v2/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/canvas-next-rsc-v2"; + +export const compositionCache = createCompositionCache(); +``` + +Pass to UniformComposition: + +```tsx + +``` + +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 + +### useUniformContext hook + +For client-side context access and updates: + +```tsx +"use client"; + +import { useUniformContext } from "@uniformdev/canvas-next-rsc-v2/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"; -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 +import { useQuirks } from "@uniformdev/canvas-next-rsc-v2/component"; + +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/canvas-next-rsc-v2/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"; + +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; +}; +``` + +Pass to UniformContext: + +```tsx + +``` -##### Asset parameters/fields +## Asset Parameters -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. +Use `flattenValues` from `@uniformdev/canvas`: ```tsx import { AssetParamValue, flattenValues } from "@uniformdev/canvas"; +import { ComponentParameter, ComponentProps } from "@uniformdev/canvas-next-rsc-v2/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 +616,9 @@ function MyComponent({ } ``` -4. Do NOT create custom utility functions like `getAssetValues` when `flattenValues` already exists and works perfectly. +**CRITICAL:** Always use `flattenValues` from `@uniformdev/canvas`. Do NOT create custom utility functions. -### Configuring Contextual Editing Live Preview - -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 +627,223 @@ import { createPreviewGETRouteHandler, createPreviewPOSTRouteHandler, createPreviewOPTIONSRouteHandler, -} from "@uniformdev/canvas-next-rsc/handler"; +} from "@uniformdev/canvas-next-rsc-v2/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/canvas-next-rsc-v2"; + +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/canvas-next-rsc-v2"; +``` + +### 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/canvas-next-rsc-v2` | +| **Core** | `resolveRouteFromCode`, `resolvePlaygroundRoute`, `precomputeComposition` | `@uniformdev/canvas-next-rsc-v2` | +| **Core** | `createUniformStaticParams`, `createCompositionCache` | `@uniformdev/canvas-next-rsc-v2` | +| **Core** | `ResolveComponentFunction`, `ResolveComponentResult` | `@uniformdev/canvas-next-rsc-v2` | +| **Clients** | `getCanvasClient`, `getRouteClient`, `getManifest`, `getManifestClient`, `getProjectMapClient` | `@uniformdev/canvas-next-rsc-v2` | +| **Components** | `UniformSlot`, `UniformText`, `UniformRichText`, `getUniformSlot` | `@uniformdev/canvas-next-rsc-v2/component` | +| **Types** | `ComponentProps`, `ComponentParameter`, `ComponentContext` | `@uniformdev/canvas-next-rsc-v2/component` | +| **Hooks** | `useUniformContext`, `useQuirks`, `useScores` | `@uniformdev/canvas-next-rsc-v2/component` | +| **Middleware** | `uniformMiddleware`, `handleUniformRoute` | `@uniformdev/canvas-next-rsc-v2/middleware` | +| **Config** | `withUniformConfig`, `UniformServerConfig` | `@uniformdev/canvas-next-rsc-v2/config` | +| **Handlers** | `createPreviewGETRouteHandler`, `createPreviewPOSTRouteHandler`, `createPreviewOPTIONSRouteHandler` | `@uniformdev/canvas-next-rsc-v2/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. From 75cc8b185d30f82a1705de00e27bf9671d5dc22e Mon Sep 17 00:00:00 2001 From: Alex Shyba Date: Thu, 18 Dec 2025 12:42:40 -0800 Subject: [PATCH 2/6] Add Next.js app router SDK v2 migration guide Updated migration guide for Next.js app router SDK v2, including installation instructions, middleware setup, new file structure, and changes to ComponentProps and UniformSlot. --- rules/app-router-sdk-v2-migration | 311 ++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 rules/app-router-sdk-v2-migration diff --git a/rules/app-router-sdk-v2-migration b/rules/app-router-sdk-v2-migration new file mode 100644 index 0000000..18c9492 --- /dev/null +++ b/rules/app-router-sdk-v2-migration @@ -0,0 +1,311 @@ +--- +description: Next.js app router SDK v2 migration guide +globs +alwaysApply: false +--- + +# Next.js app router SDK v2 migration guide + +### Install Packages + +To install packages you need `NPM_TOKEN` that has permission to them: + +```jsx + "@uniformdev/canvas-next-rsc-v2": "20.7.1-alpha.106", + "@uniformdev/canvas-next-rsc-shared-v2": "20.7.1-alpha.106", + "@uniformdev/canvas-next-rsc-client-v2": "20.7.1-alpha.106", +``` + +Make sure you have this in your `.npmrc` file in the project root and NPM_TOKEN env var set to the value provided. + +```jsx +//registry.npmjs.org/:_authToken=${NPM_TOKEN} +``` + +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/canvas-next-rsc-v2/middleware'; + +export default uniformMiddleware(); +``` + +(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/canvas-next-rsc-v2/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}`; + }, + }); +}; + +``` + +### 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 { + resolveRouteFromCode, + UniformComposition, + UniformPageParameters, +} from "@uniformdev/canvas-next-rsc-v2"; +import { notFound } from "next/navigation"; + +import { resolveComponent } from "../../../components/resolveComponent"; + +// optionally, enable ahead-of time build-time static generation of specific paths +export const generateStaticParams = async () => { + return createUniformStaticParams({ + paths: ["/"], + }); +}; + +export default async function UniformPage(props: UniformPageParameters) { + const result = await resolveRouteFromCode(props); + + if (!result.route) { + notFound(); + } + + // optional, when used tests and personalizations + // will only be evaluated here and not on the client + // await precomputeComposition(result); + + 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/canvas-next-rsc-v2"; + +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/canvas-next-rsc-v2/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, + }); + + console.log(resolved?.parameters); + + 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/canvas-next-rsc-v2/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/canvas-next-rsc-v2/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` From 3268fb4ab6c176b2187afc86423aea8f870a4c55 Mon Sep 17 00:00:00 2001 From: Alex Shyba Date: Thu, 18 Dec 2025 12:42:54 -0800 Subject: [PATCH 3/6] Rename app-router-sdk-v2-migration to app-router-sdk-v2-migration.mdc --- ...pp-router-sdk-v2-migration => app-router-sdk-v2-migration.mdc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rules/{app-router-sdk-v2-migration => app-router-sdk-v2-migration.mdc} (100%) diff --git a/rules/app-router-sdk-v2-migration b/rules/app-router-sdk-v2-migration.mdc similarity index 100% rename from rules/app-router-sdk-v2-migration rename to rules/app-router-sdk-v2-migration.mdc From 14afd81eef4906172aa434c7e071a15786e60397 Mon Sep 17 00:00:00 2001 From: Alex Shyba Date: Tue, 20 Jan 2026 16:55:41 -0800 Subject: [PATCH 4/6] Revise Next.js app router SDK v2 migration guide Updated migration guide for Next.js app router SDK v2, including package installation instructions, middleware configuration, and new file structure requirements. --- rules/app-router-sdk-v2-migration.mdc | 128 +++++++++++++++++--------- 1 file changed, 87 insertions(+), 41 deletions(-) diff --git a/rules/app-router-sdk-v2-migration.mdc b/rules/app-router-sdk-v2-migration.mdc index 18c9492..fe98445 100644 --- a/rules/app-router-sdk-v2-migration.mdc +++ b/rules/app-router-sdk-v2-migration.mdc @@ -1,25 +1,14 @@ --- -description: Next.js app router SDK v2 migration guide -globs -alwaysApply: false +alwaysApply: true --- - # Next.js app router SDK v2 migration guide ### Install Packages -To install packages you need `NPM_TOKEN` that has permission to them: - -```jsx - "@uniformdev/canvas-next-rsc-v2": "20.7.1-alpha.106", - "@uniformdev/canvas-next-rsc-shared-v2": "20.7.1-alpha.106", - "@uniformdev/canvas-next-rsc-client-v2": "20.7.1-alpha.106", -``` - -Make sure you have this in your `.npmrc` file in the project root and NPM_TOKEN env var set to the value provided. +Install the following packages with npm: `npm i @uniformdev/next-app-router --save` ```jsx -//registry.npmjs.org/:_authToken=${NPM_TOKEN} + "@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. @@ -29,15 +18,24 @@ It’s best to reference all three in your `package.json` to avoid version misma Middleware is now required to route requests and evaluate compositions, the basic structure should look like this: ```jsx -import { uniformMiddleware } from '@uniformdev/canvas-next-rsc-v2/middleware'; +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/canvas-next-rsc-v2/middleware"; +import { handleUniformRoute } from "@uniformdev/next-app-router/middleware"; import { NextRequest } from "next/server"; export default (request: NextRequest) => { @@ -55,6 +53,25 @@ export default (request: NextRequest) => { ``` +### 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, + experimental: { + 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: @@ -68,44 +85,73 @@ The default implementation of `page.tsx` should be: ```jsx import { - resolveRouteFromCode, UniformComposition, UniformPageParameters, -} from "@uniformdev/canvas-next-rsc-v2"; -import { notFound } from "next/navigation"; + createUniformStaticParams, + resolveRouteFromCode, +} from "@uniformdev/next-app-router"; +import { CustomUniformClientContext } from "@/components/CustomUniformClientContext"; import { resolveComponent } from "../../../components/resolveComponent"; -// optionally, enable ahead-of time build-time static generation of specific paths export const generateStaticParams = async () => { return createUniformStaticParams({ - paths: ["/"], + // paths: ["/"], + // Important: for localized sites, you need to add the locales to the paths + paths: ["/en"], }); }; export default async function UniformPage(props: UniformPageParameters) { - const result = await resolveRouteFromCode(props); + const { code } = await props.params; + return ( + + ); +} + +``` + +## New Playground implementation + +- app + - playground + - [code] + - page.tsx - if (!result.route) { - notFound(); - } +The default implementation of `page.tsx` should be: - // optional, when used tests and personalizations - // will only be evaluated here and not on the client - // await precomputeComposition(result); +```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 @@ -217,7 +263,7 @@ If you would like to resolve the ComponentInstance data for the slot, you can us Define your cache, in a file like `lib/cache.ts` ```jsx -import { createCompositionCache } from "@uniformdev/canvas-next-rsc-v2"; +import { createCompositionCache } from "@uniformdev/next-app-router"; export const compositionCache = createCompositionCache(); ``` @@ -238,7 +284,7 @@ Then in your components, you can resolve component data including components tha import { ComponentProps, UniformSlot, -} from "@uniformdev/canvas-next-rsc-v2/component"; +} from "@uniformdev/next-app-router/component"; import { QuirkButton } from "./custom/QuirkButton"; import { compositionCache } from "@/lib/cache"; @@ -273,7 +319,7 @@ export const Page = ({ 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/canvas-next-rsc-v2/compat'; +import { createAdapterResolveComponentFunction } from '@uniformdev/next-app-router/compat'; import * as mappings from './mappings'; @@ -287,7 +333,7 @@ Adjust your mappings to use `ResolveComponentResultWithType` and adding `mode: ' import type { ComponentProps, ResolveComponentResultWithType, -} from '@uniformdev/canvas-next-rsc-v2/compat'; +} from '@uniformdev/next-app-router/compat'; type PageComponentParameters = { text: string; From 9e2c1019b73609a0e06fe77eb098c5b347881b94 Mon Sep 17 00:00:00 2001 From: Alex Shyba Date: Tue, 20 Jan 2026 16:55:59 -0800 Subject: [PATCH 5/6] Migrate to @uniformdev/next-app-router package Updated references to the new @uniformdev/next-app-router package and adjusted npm package requirements. Modified examples and configuration to align with the new router. --- rules/uniform-next-app-router.mdc | 124 +++++++++++++++--------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/rules/uniform-next-app-router.mdc b/rules/uniform-next-app-router.mdc index 0a77f65..de513ff 100644 --- a/rules/uniform-next-app-router.mdc +++ b/rules/uniform-next-app-router.mdc @@ -10,14 +10,12 @@ This document details how to use Uniform SDK with React.js and Next.js App Route @uniform.mdc describes general Uniform principles and practices. @uniform-sdk.mdc describes framework-agnostic developer principles. -## Required npm packages +## Required npm packages (version 20.48.0 or later) ```json { - "@uniformdev/canvas-next-rsc-v2": "^20.7.1", - "@uniformdev/canvas-next-rsc-client-v2": "^20.7.1", - "@uniformdev/canvas-next-rsc-shared-v2": "^20.7.1", - "@uniformdev/context": "^20.7.1" + "@uniformdev/next-app-router": "20.48.0", + "@uniformdev/context": "20.48.0", } ``` @@ -44,18 +42,14 @@ next.config.ts # Next.js config with withUniformConfig ## Server Configuration -### uniform.server.config.ts +### uniform.server.config.ts is optional now: ```ts -import { UniformServerConfig } from "@uniformdev/canvas-next-rsc-v2/config"; +import { UniformServerConfig } from "@uniformdev/next-app-router/config"; const config: UniformServerConfig = { defaultConsent: true, - playgroundPath: "/playground", - experimental: { - quirkSerialization: true, - middlewareRuntimeCache: true, - }, + playgroundPath: "/playground" }; export default config; @@ -99,7 +93,7 @@ const config: UniformServerConfig = { ### next.config.ts ```ts -import { withUniformConfig } from "@uniformdev/canvas-next-rsc-v2/config"; +import { withUniformConfig } from "@uniformdev/next-app-router/config"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { @@ -124,7 +118,7 @@ export const runtime = 'experimental-edge'; ### Basic middleware ```ts -import { uniformMiddleware } from "@uniformdev/canvas-next-rsc-v2/middleware"; +import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; export default uniformMiddleware(); @@ -135,7 +129,7 @@ export const runtime = 'experimental-edge'; ### Middleware with locale handling ```ts -import { uniformMiddleware } from "@uniformdev/canvas-next-rsc-v2/middleware"; +import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; const defaultLocale = "en"; const locales = ["en"]; @@ -166,7 +160,7 @@ export const runtime = 'experimental-edge'; ### Full middleware options ```ts -import { uniformMiddleware } from "@uniformdev/canvas-next-rsc-v2/middleware"; +import { uniformMiddleware } from "@uniformdev/next-app-router/middleware"; export default uniformMiddleware({ // Rewrite request path before route resolution @@ -202,38 +196,32 @@ export const runtime = 'experimental-edge'; `app/uniform/[code]/page.tsx`: ```tsx -import { Suspense } from "react"; import { resolveRouteFromCode, UniformComposition, UniformPageParameters, createUniformStaticParams, - UniformContext, -} from "@uniformdev/canvas-next-rsc-v2"; -import { notFound } from "next/navigation"; +} from "@uniformdev/next-app-router"; import { resolveComponent } from "@/components/resolveComponent"; // Enable ISR (Incremental Static Regeneration) export const generateStaticParams = async () => { return createUniformStaticParams({ - paths: ["/"], + // paths: ["/"], + // Important: for localized sites, you need to add the locales to the paths + paths: ["/en"], }); }; export default async function UniformPage(props: UniformPageParameters) { - const result = await resolveRouteFromCode(props); - - if (!result.route) { - notFound(); - } - + const { code } = await props.params; return ( - <> - - - - - + ); } ``` @@ -249,16 +237,28 @@ export default async function UniformPage(props: UniformPageParameters) { ```tsx import { + createUniformPlaygroundStaticParams, + PlaygroundParameters, resolvePlaygroundRoute, - UniformPageParameters, UniformPlayground, -} from "@uniformdev/canvas-next-rsc-v2"; +} from "@uniformdev/next-app-router"; + import { resolveComponent } from "@/components/resolveComponent"; -export default async function PlaygroundPage(props: UniformPageParameters) { - const result = await resolvePlaygroundRoute(props); +export const generateStaticParams = async () => { + return createUniformPlaygroundStaticParams({ + paths: ["/en"], + }); +}; + +export default async function PlaygroundPage({ params }: PlaygroundParameters) { + const { code } = await params; return ( - + ); } ``` @@ -296,7 +296,7 @@ export default function RootLayout({ import { ResolveComponentFunction, type ResolveComponentResult, -} from "@uniformdev/canvas-next-rsc-v2"; +} from "@uniformdev/next-app-router"; import { DefaultNotFoundComponent } from "./default"; import { HeroComponent } from "./hero"; @@ -318,7 +318,7 @@ export const resolveComponent: ResolveComponentFunction = ({ component }) => { Default fallback component: ```tsx -import { ComponentProps } from "@uniformdev/canvas-next-rsc-v2/component"; +import { ComponentProps } from "@uniformdev/next-app-router/component"; export const DefaultNotFoundComponent = ({ type }: ComponentProps) => { return
Not Found: {type}
; @@ -340,7 +340,7 @@ import { ComponentProps, UniformText, UniformRichText, -} from "@uniformdev/canvas-next-rsc-v2/component"; +} from "@uniformdev/next-app-router/component"; export type HeroProps = { title?: ComponentParameter; @@ -383,7 +383,7 @@ const HeroComponent = ({ parameters: { title } }: ComponentProps) => ## Rendering Slots ```tsx -import { ComponentProps, UniformSlot } from "@uniformdev/canvas-next-rsc-v2/component"; +import { ComponentProps, UniformSlot } from "@uniformdev/next-app-router/component"; export type PageProps = unknown; export type PageSlots = "content" | "header" | "footer"; @@ -418,7 +418,7 @@ export const PageComponent = ({ slots }: ComponentProps) = Alternative for extracting slot items as array: ```tsx -import { getUniformSlot } from "@uniformdev/canvas-next-rsc-v2/component"; +import { getUniformSlot } from "@uniformdev/next-app-router/component"; const items = getUniformSlot({ slot: slots.content }); // Returns ReactNode[] | undefined @@ -429,7 +429,7 @@ const items = getUniformSlot({ slot: slots.content }); Use composition cache to access ComponentInstance data: ```tsx -import { createCompositionCache } from "@uniformdev/canvas-next-rsc-v2"; +import { createCompositionCache } from "@uniformdev/next-app-router"; export const compositionCache = createCompositionCache(); ``` @@ -465,7 +465,7 @@ For client-side context access and updates: ```tsx "use client"; -import { useUniformContext } from "@uniformdev/canvas-next-rsc-v2/component"; +import { useUniformContext } from "@uniformdev/next-app-router/component"; import { useEffect, useState } from "react"; export const QuirkButton = () => { @@ -501,7 +501,7 @@ Reactive access to visitor quirk values: ```tsx "use client"; -import { useQuirks } from "@uniformdev/canvas-next-rsc-v2/component"; +import { useQuirks } from "@uniformdev/next-app-router/component"; export const LocationBanner = () => { const quirks = useQuirks(); @@ -517,7 +517,7 @@ Reactive access to visitor score values: ```tsx "use client"; -import { useScores } from "@uniformdev/canvas-next-rsc-v2/component"; +import { useScores } from "@uniformdev/next-app-router/component"; export const InterestIndicator = () => { const scores = useScores(); @@ -590,7 +590,7 @@ Use `flattenValues` from `@uniformdev/canvas`: ```tsx import { AssetParamValue, flattenValues } from "@uniformdev/canvas"; -import { ComponentParameter, ComponentProps } from "@uniformdev/canvas-next-rsc-v2/component"; +import { ComponentParameter, ComponentProps } from "@uniformdev/next-app-router/component"; interface MyComponentProps { multipleImages?: ComponentParameter; @@ -627,7 +627,7 @@ import { createPreviewGETRouteHandler, createPreviewPOSTRouteHandler, createPreviewOPTIONSRouteHandler, -} from "@uniformdev/canvas-next-rsc-v2/handler"; +} from "@uniformdev/next-app-router/handler"; export const GET = createPreviewGETRouteHandler({ resolveFullPath: ({ path }) => (path ? path : "/playground"), @@ -646,7 +646,7 @@ import { UniformComposition, UniformContext, precomputeComposition, -} from "@uniformdev/canvas-next-rsc-v2"; +} from "@uniformdev/next-app-router"; export default async function UniformPage(props: UniformPageParameters) { const result = await resolveRouteFromCode(props); @@ -691,7 +691,7 @@ import { getManifestClient, getProjectMapClient, getRouteClient, -} from "@uniformdev/canvas-next-rsc-v2"; +} from "@uniformdev/next-app-router"; ``` ### Examples @@ -818,17 +818,17 @@ type ComponentParameter = BaseComponentParameter & { | Category | Export | Import Path | |----------|--------|-------------| -| **Core** | `UniformComposition`, `UniformContext`, `UniformPlayground` | `@uniformdev/canvas-next-rsc-v2` | -| **Core** | `resolveRouteFromCode`, `resolvePlaygroundRoute`, `precomputeComposition` | `@uniformdev/canvas-next-rsc-v2` | -| **Core** | `createUniformStaticParams`, `createCompositionCache` | `@uniformdev/canvas-next-rsc-v2` | -| **Core** | `ResolveComponentFunction`, `ResolveComponentResult` | `@uniformdev/canvas-next-rsc-v2` | -| **Clients** | `getCanvasClient`, `getRouteClient`, `getManifest`, `getManifestClient`, `getProjectMapClient` | `@uniformdev/canvas-next-rsc-v2` | -| **Components** | `UniformSlot`, `UniformText`, `UniformRichText`, `getUniformSlot` | `@uniformdev/canvas-next-rsc-v2/component` | -| **Types** | `ComponentProps`, `ComponentParameter`, `ComponentContext` | `@uniformdev/canvas-next-rsc-v2/component` | -| **Hooks** | `useUniformContext`, `useQuirks`, `useScores` | `@uniformdev/canvas-next-rsc-v2/component` | -| **Middleware** | `uniformMiddleware`, `handleUniformRoute` | `@uniformdev/canvas-next-rsc-v2/middleware` | -| **Config** | `withUniformConfig`, `UniformServerConfig` | `@uniformdev/canvas-next-rsc-v2/config` | -| **Handlers** | `createPreviewGETRouteHandler`, `createPreviewPOSTRouteHandler`, `createPreviewOPTIONSRouteHandler` | `@uniformdev/canvas-next-rsc-v2/handler` | +| **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` | From 79aa2dc0dda9044357dc6df9377147dc9ddc1d29 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 14 Apr 2026 10:47:34 -0700 Subject: [PATCH 6/6] fix: approuter sdk v2 rules --- rules/app-router-sdk-v2-migration.mdc | 8 ++--- .../solution-architecture.mdc | 1 - rules/uniform-next-app-router.mdc | 33 +++++-------------- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/rules/app-router-sdk-v2-migration.mdc b/rules/app-router-sdk-v2-migration.mdc index fe98445..914024a 100644 --- a/rules/app-router-sdk-v2-migration.mdc +++ b/rules/app-router-sdk-v2-migration.mdc @@ -62,10 +62,8 @@ import { UniformServerConfig } from "@uniformdev/next-app-router/config"; const config: UniformServerConfig = { defaultConsent: true, - experimental: { - quirkSerialization: true, - middlewareRuntimeCache: true, - }, + quirkSerialization: true, + middlewareRuntimeCache: true, playgroundPath: "/playground", }; @@ -302,8 +300,6 @@ export const Page = ({ compositionId: context._id, }); - console.log(resolved?.parameters); - return resolved; }); 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 de513ff..3630c72 100644 --- a/rules/uniform-next-app-router.mdc +++ b/rules/uniform-next-app-router.mdc @@ -10,12 +10,12 @@ This document details how to use Uniform SDK with React.js and Next.js App Route @uniform.mdc describes general Uniform principles and practices. @uniform-sdk.mdc describes framework-agnostic developer principles. -## Required npm packages (version 20.48.0 or later) +## Required npm packages (version 20.58.0 or later) ```json { - "@uniformdev/next-app-router": "20.48.0", - "@uniformdev/context": "20.48.0", + "@uniformdev/next-app-router": "20.58.0", + "@uniformdev/context": "20.58.0", } ``` @@ -65,11 +65,6 @@ const config: UniformServerConfig = { // Path to playground page playgroundPath: "/playground", - // Cache settings for different data types - manifestCache: { type: "force-cache" }, - canvasCache: { type: "revalidate", interval: 60 }, - projectMapCache: { type: "force-cache" }, - // ETag support for caching eTags: { generateETags: true, @@ -81,12 +76,10 @@ const config: UniformServerConfig = { disableDevTools: false, }, - // Experimental features - experimental: { - quirkSerialization: true, - middlewareRuntimeCache: true, - disableSwrMiddlewareCache: false, - }, + // Quirk and middleware options + quirkSerialization: true, + middlewareRuntimeCache: true, + disableSwrMiddlewareCache: false, }; ``` @@ -434,17 +427,7 @@ import { createCompositionCache } from "@uniformdev/next-app-router"; export const compositionCache = createCompositionCache(); ``` -Pass to UniformComposition: - -```tsx - -``` - -Then in components: +Pass `compositionCache` as a prop to `UniformComposition` (see Main Composition Route above), then in components: ```tsx const headerItems = slots.header.items.map((item) => {