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 (
+
+ )
+}
+```
+
+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