# Uniform SDK for Nuxt

> Comprehensive guide to integrating Uniform with Nuxt, including setup, component development, personalization, and visual editing.

Source: https://docs.uniform.app/docs/sdk/nuxt

The Uniform SDK for Nuxt (`@uniformdev/uniform-nuxt`) provides a first-class integration between Uniform's visual workspace and Nuxt. It handles route resolution, component rendering, personalization, A/B testing, and visual editing -- all through a dedicated Nuxt module and Vue composables.

## How it works

Understanding the request lifecycle helps you reason about configuration choices and debug issues effectively:

1. **A visitor requests a page** -- When a visitor navigates to a URL (e.g., `/about`), Nuxt's catch-all route receives the request and resolves the path against Uniform's Project Map.
2. **The composition is fetched** -- The `useUniformComposition` composable fetches the full composition data from the Uniform API based on the resolved Project Map node path.
3. **Components render via the composition tree** -- `UniformComposition` walks the composition tree and uses your `resolveRenderer` function to map each Uniform component type to a Vue component. Parameters are flattened and passed directly as props.
4. **Personalization and testing are evaluated** -- The `UniformContextProvider` (set up automatically by the Nuxt module) evaluates personalization rules and A/B tests client-side or server-side, depending on your `outputType` configuration.
5. **Visual editing works out of the box** -- When previewing in the Uniform dashboard, the SDK automatically enables contextual editing, inline text editing, and live preview via the playground route.

---

## Getting started

### Prerequisites

- Nuxt 4+ (Vue 3)
- Node.js 18+
- A Uniform project with compositions set up

### Install packages

```bash
npm install @uniformdev/uniform-nuxt@latest @uniformdev/canvas@latest @uniformdev/canvas-vue@latest @uniformdev/context@latest @uniformdev/context-vue@latest @uniformdev/project-map@latest
```

You also need the Uniform CLI as a dev dependency for syncing content and downloading manifests:

```bash
npm install -D @uniformdev/cli@latest
```

### Environment variables

Create a `.env` file in your project root:

```bash
UNIFORM_API_KEY=your-api-key
UNIFORM_PROJECT_ID=your-project-id
```

> **Note:**
>
> You can find these values in your Uniform project under **Security > Service Accounts**. See the [API access guide](https://docs.uniform.app/docs/guides/api-access) for more information.

---

## Project setup

The Uniform SDK requires a specific set of files to wire up routing, rendering, and preview. Here is the minimal file structure:

```
your-nuxt-app/
├── app.vue                          # Root app (standard Nuxt)
├── pages/
│   ├── [...path].vue                # Catch-all route for compositions
│   └── playground.vue               # Playground route (visual editing)
├── components/
│   ├── canvasComponents.ts          # Maps Uniform types to Vue components
│   ├── Page.vue                     # Example page component
│   └── Hero.vue                     # Example hero component
├── lib/
│   └── uniform/
│       └── contextManifest.json     # Downloaded personalization manifest
├── server/
│   └── middleware/
│       └── cors.ts                  # CORS middleware (required for preview)
├── nuxt.config.ts                   # Nuxt config with Uniform module
└── uniform.config.ts                # Uniform CLI configuration
```

The following sections walk through each file.

### Step 1: Nuxt configuration

Register the `@uniformdev/uniform-nuxt` module and configure it in `nuxt.config.ts`:

```ts
// nuxt.config.ts
import manifest from "./lib/uniform/contextManifest.json";
import type { ManifestV2 } from "@uniformdev/context";

export default defineNuxtConfig({
  compatibilityDate: "2025-05-14",
  modules: ["@uniformdev/uniform-nuxt"],
  uniform: {
    projectId: process.env.UNIFORM_PROJECT_ID,
    readOnlyApiKey: process.env.UNIFORM_API_KEY,
    apiHost: process.env.UNIFORM_CLI_BASE_URL,
    edgeApiHost: process.env.UNIFORM_CLI_BASE_EDGE_URL,
    manifest: manifest as ManifestV2,
    defaultConsent: true,
    outputType: "standard",
    playgroundPath: "/playground",
  },
});
```

The Nuxt module automatically:

- Sets up the `UniformContextProvider` around your app
- Registers `UniformComposition`, `UniformPlayground`, `UniformSlot`, `UniformText`, and `UniformRichText` as global components (no imports needed in templates)
- Provides the `useUniformComposition` composable
- Exposes the Uniform configuration via `useRuntimeConfig().public.$uniform`

#### Uniform module options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `projectId` | `string` | -- | Your Uniform project ID |
| `readOnlyApiKey` | `string` | -- | API key with read access |
| `apiHost` | `string` | `undefined` | Custom API host URL |
| `edgeApiHost` | `string` | `undefined` | Custom edge API host URL |
| `manifest` | `ManifestV2` | -- | The personalization manifest object |
| `defaultConsent` | `boolean` | `false` | Default storage consent for new visitors |
| `outputType` | `"standard" \| "edge"` | `"standard"` | How personalization variants are output |
| `playgroundPath` | `string` | `"/playground"` | Path to the playground page handler |

> **Note:**
>
> The `outputType` setting controls how personalization and A/B test variants are rendered:
>
> - **`standard`** -- Evaluates personalization server-side during SSR or client-side, outputs only the winning variant. Use for standard SSR/SSG deployments.
> - **`edge`** -- Outputs all variants in the HTML with metadata, suitable for edge-side personalization where an edge worker selects the winning variant.

### Step 2: Catch-all route

Create `pages/[...path].vue`. This catch-all route handles all Uniform-managed pages:

```vue
<!-- pages/[...path].vue -->
<script lang="ts" setup>
const route = useRoute();
const path = Array.isArray(route.params.path)
  ? `/${route.params.path.join("/")}`
  : `/${route.params.path}`;

const { composition, error } = await useUniformComposition({
  projectMapNodePath: path,
});

if (error?.value) {
  console.error("Error fetching composition from Uniform", error.value);
}
</script>

<template>
  <UniformComposition
    v-if="composition"
    :data="composition"
    :resolveRenderer="resolveRenderer"
  />
</template>
```

Key points:

- `useUniformComposition` is an async composable provided by the Nuxt module. It fetches the composition data for the given Project Map node path.
- The returned `composition` is a reactive ref containing the `RootComponentInstance` data.
- If the composition cannot be found, `error` will contain the error details.

#### useUniformComposition options

| Option | Type | Description |
| --- | --- | --- |
| `projectMapNodePath` | `string` | The Project Map node path to resolve (e.g., `/about`) |

### Step 3: Playground route

Create `pages/playground.vue`. The playground route enables visual editing and previewing individual compositions from the Uniform dashboard:

```vue
<!-- pages/playground.vue -->
<script lang="ts" setup>
import { resolveRenderer } from "../components/canvasComponents";
</script>

<template>
  <UniformPlayground :resolveRenderer="resolveRenderer" />
</template>
```

The `UniformPlayground` component renders an empty composition shell that the Uniform Canvas editor populates during contextual editing sessions. It uses the same `resolveRenderer` function as your main composition route.

### Step 4: Component resolution

Create `components/canvasComponents.ts`. This function maps Uniform component types (defined in your Uniform project) to Vue components:

```ts
// components/canvasComponents.ts
import type { ComponentInstance } from "@uniformdev/canvas";
import {
  DefaultNotImplementedComponent,
  type ResolveRenderer,
} from "@uniformdev/canvas-vue";
import Page from "./Page.vue";
import Hero from "./Hero.vue";

const componentMap = {
  hero: Hero,
  page: Page,
};

export const resolveRenderer: ResolveRenderer = (component) => {
  return (
    componentMap[component.type as keyof typeof componentMap] ??
    DefaultNotImplementedComponent
  );
};
```

The `resolveRenderer` function receives a `ComponentInstance` and returns the Vue component to render. It is called for every component in the composition tree.

`DefaultNotImplementedComponent` is a built-in fallback that renders a helpful message when a component type has no registered implementation. Always include it as a fallback to gracefully handle unknown component types during development.

> **Warning:**
>
> The `resolveRenderer` function signature differs from the Next.js SDK. In Nuxt, it returns a Vue `Component` directly (not a `{ component }` result object).

### Step 5: CORS middleware for preview

Create `server/middleware/cors.ts`. This server middleware is required for the Uniform Canvas visual editor to communicate with your Nuxt app during preview:

```ts
// server/middleware/cors.ts
export default defineEventHandler((event) => {
  setResponseHeaders(event, {
    "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Expose-Headers": "*",
  });
  if (getMethod(event) === "OPTIONS") {
    event.res.statusCode = 204;
    event.res.statusMessage = "No Content.";
    return "OK";
  }
});
```

### Step 6: Root app

Your root `app.vue` is a standard Nuxt app file. The Uniform module handles context setup automatically, so no Uniform-specific components are needed here:

```vue
<!-- app.vue -->
<template>
  <NuxtPage />
</template>
```

---

## Building components

Uniform components in Nuxt receive **flattened props** -- parameter values are extracted from their `ComponentParameter` wrappers and spread directly onto the component. For example, if a Uniform component has a `title` parameter of type `text`, your Vue component receives a `title` prop with the string value directly.

Every component also receives a `component` prop containing the full `ComponentInstance`, which provides access to raw parameter data, component metadata, and slots.

### Component props

Every component rendered via `resolveRenderer` receives these props:

| Prop | Type | Description |
| --- | --- | --- |
| `component` | `ComponentInstance` | The full component instance with raw parameters, slots, and metadata |
| `[parameterName]` | flattened value | Each parameter's `.value` is spread as a top-level prop |

### Basic component with parameters

Parameters defined in the Uniform dashboard are passed as flattened props. Define your component's props interface using the expected value types:

```vue
<!-- components/Hero.vue -->
<script lang="ts" setup>
import type { ComponentInstance } from "@uniformdev/canvas";
import type { UniformRichTextNodeProps } from "@uniformdev/canvas-vue";

interface Props {
  title: string;
  description?: UniformRichTextNodeProps["node"];
  component: ComponentInstance;
}

defineProps<Props>();
</script>

<template>
  <div>
    <UniformText
      parameterId="title"
      as="h1"
      class="title"
      placeholder="Enter a title"
    />
    <UniformRichText
      parameter-id="description"
      as="div"
      class="description"
      placeholder="Enter a description"
    />
  </div>
</template>
```

> **Note:**
>
> **Props are flattened automatically.** Unlike the Next.js SDK where you access `parameters.title?.value`, in Nuxt your component receives `title` directly as a prop with the raw value. The `component` prop gives you access to the full `ComponentInstance` when you need raw parameter metadata.

### Accessing raw parameter values

When you need the full parameter metadata (e.g., for conditional logic based on parameter type or contextual editing state), access the `component` prop:

```vue
<script lang="ts" setup>
import type { ComponentInstance } from "@uniformdev/canvas";

interface Props {
  title: string;
  linkUrl?: string;
  isVisible?: boolean;
  component: ComponentInstance;
}

const props = defineProps<Props>();

// Access the raw parameter with full metadata
const titleParam = props.component.parameters?.title;
const titleValue = titleParam?.value as string;

// Or use the flattened prop directly
const displayTitle = props.title ?? "Default title";
</script>

<template>
  <a v-if="isVisible !== false" :href="linkUrl ?? '#'">
    <h2>{{ displayTitle }}</h2>
  </a>
</template>
```

### UniformText

`UniformText` renders text parameters with built-in support for inline editing in the Uniform visual editor. It is registered globally by the Nuxt module, so no import is needed in templates:

```vue
<UniformText
  parameterId="title"          <!-- Required: the parameter ID string -->
  as="h1"                      <!-- Optional: HTML element (default: "span") -->
  class="text-xl"              <!-- Optional: CSS class -->
  placeholder="Enter title"    <!-- Optional: placeholder shown in editor when empty -->
  :isMultiline="false"         <!-- Optional: enables multi-line editing (default: false) -->
/>
```

#### UniformText props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `parameterId` | `string` | -- | **Required.** The ID of the parameter to render |
| `as` | `string` | `"span"` | The HTML element to render |
| `isMultiline` | `boolean` | `false` | When `true`, adds `white-space: pre-wrap` to support line breaks |
| `placeholder` | `string \| ((param: { id: string }) => string)` | `undefined` | Placeholder text shown in the Canvas editor when the value is empty |

#### Custom rendering with the default slot

`UniformText` supports a default scoped slot for custom value rendering:

```vue
<UniformText parameterId="title" as="h1">
  <template #default="value">
    {{ value?.toUpperCase() }}
  </template>
</UniformText>
```

### UniformRichText

`UniformRichText` renders rich text parameters with full formatting support. It is also globally registered:

```vue
<UniformRichText
  parameter-id="description"          <!-- Required: the parameter ID -->
  as="div"                             <!-- Optional: wrapper element -->
  class="prose"                        <!-- Optional: CSS class -->
  placeholder="Enter description"      <!-- Optional: placeholder in editor -->
  :resolveRichTextRenderer="customResolver"  <!-- Optional: custom node renderers -->
/>
```

#### UniformRichText props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `parameterId` | `string` | -- | **Required.** The ID of the rich text parameter |
| `as` | `keyof HTMLElementTagNameMap` | `undefined` | Optional wrapper element. If not set, no wrapper is rendered |
| `placeholder` | `string \| ((param: { id: string }) => string)` | `undefined` | Placeholder text shown when the value is empty |
| `resolveRichTextRenderer` | `RenderRichTextComponentResolver` | `undefined` | Custom function to override default rich text node renderers |

#### Custom rich text node rendering

Override how specific rich text node types are rendered using `resolveRichTextRenderer`:

```vue
<script lang="ts" setup>
import type { RichTextNode } from "@uniformdev/richtext";
import type { Component } from "vue";
import CustomHeading from "./CustomHeading.vue";

const resolveRichTextRenderer = (node: RichTextNode): Component | null => {
  if (node.type === "heading") {
    return CustomHeading;
  }
  // Return null/undefined to use the default renderer
  return null;
};
</script>

<template>
  <UniformRichText
    parameter-id="body"
    as="div"
    :resolveRichTextRenderer="resolveRichTextRenderer"
  />
</template>
```

Each custom renderer component receives a `node` prop with the `RichTextNode` data and a `children` slot/prop for nested nodes.

---

## Slots

Slots define where child components can be placed within a parent component. A page component typically has slots like `content`, `header`, and `footer`.

### Rendering slots with UniformSlot

`UniformSlot` is globally registered by the Nuxt module:

```vue
<!-- components/Page.vue -->
<template>
  <div>
    <header>
      <UniformSlot name="header" />
    </header>
    <main>
      <UniformSlot name="content" />
    </main>
    <footer>
      <UniformSlot name="footer" />
    </footer>
  </div>
</template>
```

When `name` is omitted, `UniformSlot` renders all slots combined:

```vue
<template>
  <UniformSlot />
</template>
```

#### UniformSlot props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | `undefined` | The slot name to render. If omitted, all slots are rendered |
| `resolveRenderer` | `ResolveRenderer` | inherited | Override the component resolver for this slot's children |

### Custom slot rendering with scoped slots

`UniformSlot` supports a default scoped slot for wrapping individual slot items:

```vue
<UniformSlot name="content">
  <template #default="{ child, component }">
    <div class="my-4 border-b pb-4">
      <component :is="child" />
    </div>
  </template>
</UniformSlot>
```

The scoped slot receives:

| Property | Type | Description |
| --- | --- | --- |
| `child` | `VNode` | The rendered child component |
| `component` | `ComponentInstance` | The raw component instance data |

### Empty slot placeholder

`UniformSlot` supports an `emptyPlaceholder` slot shown when the slot has no items:

```vue
<UniformSlot name="sidebar">
  <template #emptyPlaceholder>
    <p class="text-gray-400 italic">Drag components here</p>
  </template>
</UniformSlot>
```

---

## Asset parameters

Use `flattenValues` from `@uniformdev/canvas` to work with asset parameters:

```vue
<script lang="ts" setup>
import type { ComponentInstance } from "@uniformdev/canvas";
import { flattenValues } from "@uniformdev/canvas";

interface Props {
  component: ComponentInstance;
}

const props = defineProps<Props>();

// Multiple assets: returns an array
const images = flattenValues(props.component.parameters?.images);

// Single asset: returns a single object
const hero = flattenValues(props.component.parameters?.featuredImage, {
  toSingle: true,
});
</script>

<template>
  <div>
    <img
      v-if="hero"
      :src="hero.url"
      :width="hero.width"
      :height="hero.height"
      :alt="hero.fields?.alt?.value ?? ''"
    />
    <div class="grid grid-cols-3 gap-4">
      <img
        v-for="(img, i) in images"
        :key="i"
        :src="img?.url"
        :width="img?.width"
        :height="img?.height"
      />
    </div>
  </div>
</template>
```

> **Warning:**
>
> Always use `flattenValues` from `@uniformdev/canvas` for asset parameters. Do not create custom utility functions for extracting asset data.

---

## Composition wrapper component

For more complex page layouts where you need to wrap `UniformComposition` with navigation, metadata, or other elements, create a composition wrapper:

```vue
<!-- components/PageComposition.vue -->
<script lang="ts" setup>
import type { RootComponentInstance } from "@uniformdev/canvas";
import { resolveRenderer } from "./canvasComponents";

interface Props {
  composition: RootComponentInstance;
}

const props = defineProps<Props>();

// Access composition-level parameters
const { metaTitle } = props.composition?.parameters || {};
const title = metaTitle?.value as string;
</script>

<template>
  <div class="page">
    <Head>
      <Title>{{ title }}</Title>
    </Head>
    <Navigation />
    <UniformComposition
      :data="composition"
      :resolveRenderer="resolveRenderer"
    />
    <Footer />
  </div>
</template>
```

Then use it in your catch-all route:

```vue
<!-- pages/[...path].vue -->
<script lang="ts" setup>
const route = useRoute();
const path = Array.isArray(route.params.path)
  ? `/${route.params.path.join("/")}`
  : `/${route.params.path}`;

const { composition, error } = await useUniformComposition({
  projectMapNodePath: path,
});
</script>

<template>
  <PageComposition v-if="composition" :composition="composition" />
</template>
```

---

## Personalization and testing

Personalization and A/B testing are configured in the Uniform dashboard and evaluated automatically by the SDK. The `UniformSlot` component handles personalization and test containers transparently -- when it encounters a personalization or test component in the composition tree, it renders the appropriate `Personalize` or `Test` component from `@uniformdev/context-vue`.

### How personalization works in Nuxt

1. The personalization manifest is downloaded locally (see [Manifest management](#manifest-management)) and passed to the Nuxt module configuration.
2. The `UniformContextProvider` (set up automatically by the Nuxt module) creates a `Context` instance from the manifest.
3. When `UniformSlot` encounters a personalization container, it evaluates the visitor's scores against the variant criteria and renders the winning variant(s).
4. Score updates happen through enrichment tracking (`Track` component) and route-based signals.

### Output types

The `outputType` in your `nuxt.config.ts` controls how variants are rendered:

- **`standard`** (default) -- Evaluates personalization during SSR or on the client. Only the winning variant HTML is included in the response. Best for most deployments.
- **`edge`** -- All variant HTML is rendered with metadata tags. An edge worker (CDN-level) selects and shows the winning variant. Best for fully cached/static sites with edge personalization.

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  uniform: {
    // ...
    outputType: "standard", // or "edge"
  },
});
```

### Behavior tracking

Components with enrichment tags automatically track visitor behavior. The `behaviorTracking` prop on `UniformComposition` controls when tracking occurs:

```vue
<UniformComposition
  :data="composition"
  :resolveRenderer="resolveRenderer"
  behaviorTracking="onView"
/>
```

| Value | Description |
| --- | --- |
| `"onView"` (default) | Tracks when the component enters the viewport (uses `IntersectionObserver`). Renders a wrapping `<div>` |
| `"onLoad"` | Tracks immediately when the component mounts, regardless of viewport. No wrapping element |

---

## Client-side context

The Nuxt module automatically sets up the `UniformContextProvider` for your app. Several composables are available for client-side interactivity.

### useUniformContext

Access the Uniform Context instance directly. Available through the Nuxt app instance:

```vue
<script lang="ts" setup>
const { context } = useNuxtApp().$useUniformContext();

const forgetMe = async () => {
  await context.forget(true);
};
</script>

<template>
  <button @click="forgetMe">Forget me</button>
</template>
```

The `context` object is the `@uniformdev/context` `Context` instance, providing access to:

| Method / Property | Description |
| --- | --- |
| `context.scores` | Current visitor score values |
| `context.quirks` | Current visitor quirk values |
| `context.update(options)` | Update visitor data (quirks, enrichments, URL) |
| `context.forget(purge)` | Clear all visitor data. Pass `true` to also clear cookies |
| `context.personalize(options)` | Manually evaluate personalization |

### useQuirks

Provides reactive access to the current visitor's quirk values. The component re-renders whenever quirks change:

```vue
<script lang="ts" setup>
import { useQuirks } from "@uniformdev/context-vue";

const quirks = useQuirks();
</script>

<template>
  <div>Current country: {{ quirks?.country ?? "Unknown" }}</div>
</template>
```

### useScores

Provides reactive access to the current visitor's score values:

```vue
<script lang="ts" setup>
import { useScores } from "@uniformdev/context-vue";

const scores = useScores();
</script>

<template>
  <div>Tech interest score: {{ scores?.tech ?? 0 }}</div>
</template>
```

### Updating quirks

Update visitor quirks to trigger personalization changes:

```vue
<script lang="ts" setup>
const { context } = useNuxtApp().$useUniformContext();

const setCountry = async (country: string) => {
  await context.update({
    quirks: { country },
  });
};
</script>

<template>
  <div>
    <button @click="setCountry('Canada')">I'm from Canada</button>
    <button @click="setCountry('USA')">I'm from USA</button>
  </div>
</template>
```

---

## Project Map integration

The Uniform Project Map defines the site's URL structure and maps paths to compositions. You can query project map nodes to build navigation or resolve routes programmatically.

### Fetching navigation from Project Map

Create a composable to fetch project map nodes for navigation:

```ts
// composables/useUniformProjectMapNodes.ts
import { ProjectMapClient } from "@uniformdev/project-map";
import {
  CANVAS_DRAFT_STATE,
  CANVAS_PUBLISHED_STATE,
} from "@uniformdev/canvas";

export function useUniformProjectMapNodes() {
  const nuxtApp = useNuxtApp();
  const isPreview = nuxtApp.$preview as boolean;
  const uniformConfig = useRuntimeConfig().public.$uniform;

  return useAsyncData(
    `project-map-nodes-${isPreview}`,
    async () => {
      const projectMapClient = new ProjectMapClient({
        apiKey: uniformConfig.readOnlyApiKey,
        apiHost: uniformConfig.apiHost,
        projectId: uniformConfig.projectId,
        bypassCache: true,
      });

      const state =
        process.env.NODE_ENV === "development" || isPreview
          ? CANVAS_DRAFT_STATE
          : CANVAS_PUBLISHED_STATE;

      const response = await projectMapClient.getNodes({
        state,
        depth: 1,
      });

      return (response.nodes ?? [])
        .filter(
          (node) =>
            node.path &&
            node.type === "composition" &&
            node.compositionId
        )
        .map((node) => ({
          title: node.name,
          url: node.path,
        }));
    }
  );
}
```

Use it in a navigation component:

```vue
<!-- components/Navigation.vue -->
<script lang="ts" setup>
export interface NavLink {
  title: string;
  url: string;
}

interface Props {
  navLinks: NavLink[];
}

defineProps<Props>();
</script>

<template>
  <nav class="navigation">
    <NuxtLink
      v-for="{ url, title } in navLinks"
      :key="url"
      :href="url"
    >
      {{ title }}
    </NuxtLink>
  </nav>
</template>
```

---

## Manifest management

> **Warning:**
>
> **Nuxt requires downloading the manifest locally.** Unlike the Next.js App Router SDK which fetches the manifest at runtime, the Nuxt SDK expects the manifest to be imported as a JSON file and passed to the module configuration.

Add these scripts to your `package.json`:

```json
{
  "scripts": {
    "dev": "run-s download:manifest nuxt:dev",
    "nuxt:dev": "nuxt dev",
    "build": "run-s download:manifest nuxt:build",
    "nuxt:build": "nuxt build",
    "download:manifest": "uniform context manifest download --output ./lib/uniform/contextManifest.json",
    "uniform:push": "uniform sync push",
    "uniform:pull": "uniform sync pull",
    "uniform:publish": "uniform context manifest publish"
  }
}
```

| Script | Description |
| --- | --- |
| `download:manifest` | Downloads the personalization manifest to a local JSON file |
| `uniform:push` | Pushes local content definitions to your Uniform project |
| `uniform:pull` | Pulls content definitions from your Uniform project |
| `uniform:publish` | Publishes the personalization manifest (required after changing signals, enrichments, or quirks) |

> **Note:**
>
> The `dev` and `build` scripts use `npm-run-all` (`run-s`) to download the manifest before starting the dev server or building. Install it as a dev dependency: `npm install -D npm-run-all`.

The manifest file is imported in `nuxt.config.ts`:

```ts
import manifest from "./lib/uniform/contextManifest.json";
import type { ManifestV2 } from "@uniformdev/context";

export default defineNuxtConfig({
  uniform: {
    manifest: manifest as ManifestV2,
    // ...
  },
});
```

---

## Uniform CLI configuration

Create `uniform.config.ts` at your project root to configure the Uniform CLI for content sync:

```ts
// uniform.config.ts
import { uniformConfig } from "@uniformdev/cli/config";

module.exports = uniformConfig({
  preset: "all",
  overrides: {
    serializationConfig: { directory: "./content" },
  },
  disableEntities: ["webhook"],
});
```

This configuration:

- Uses the `all` preset to sync all entity types
- Serializes content definitions to the `./content` directory
- Disables webhook syncing (optional)

---

## Contextual editing

The SDK provides built-in support for Uniform's visual/contextual editing. When a composition is opened in the Uniform Canvas editor, the SDK automatically:

1. Listens for composition update messages from the editor
2. Re-renders the component tree with the updated data
3. Enables inline editing for `UniformText` and `UniformRichText` components
4. Annotates components with editing attributes for the Canvas UI

### useUniformContextualEditing

This composable is integrated into `UniformComposition` automatically. For custom setups, you can use it directly:

```vue
<script lang="ts" setup>
import { useUniformContextualEditing } from "@uniformdev/canvas-vue";
import { shallowRef } from "vue";

const initialData = shallowRef(compositionData);

const { composition, isContextualEditing } = useUniformContextualEditing({
  initialCompositionValue: initialData,
  enhance: (message) => message.composition,
});
</script>
```

| Return Value | Type | Description |
| --- | --- | --- |
| `composition` | `ComputedRef<RootComponentInstance>` | The current composition (updates during editing) |
| `isContextualEditing` | `ComputedRef<boolean>` | Whether the app is currently in contextual editing mode |

### Custom contextual editing enhancer

If you need to enhance the composition data during contextual editing (e.g., to run data enhancers), pass a custom `contextualEditingEnhancer` to `UniformComposition`:

```vue
<UniformComposition
  :data="composition"
  :resolveRenderer="resolveRenderer"
  :contextualEditingEnhancer="enhance"
/>
```

The enhancer function receives the update message and should return the enhanced composition:

```ts
const enhance = async (message: {
  composition: RootComponentInstance;
  hash: string;
}) => {
  // Run custom enhancers on the composition
  return message.composition;
};
```

---

## Import reference

### @uniformdev/uniform-nuxt

The Nuxt module. Register it in `nuxt.config.ts` under `modules`.

Automatically provides:

- `UniformComposition`, `UniformPlayground`, `UniformSlot`, `UniformText`, `UniformRichText` as global components
- `useUniformComposition` composable
- `UniformContextProvider` wrapping the app
- Runtime config under `useRuntimeConfig().public.$uniform`

### @uniformdev/canvas-vue

Component and utility exports for Canvas rendering:

| Export | Description |
| --- | --- |
| `UniformComposition` | Main composition renderer component |
| `UniformPlayground` | Playground composition renderer for visual editing |
| `UniformComponent` | Internal component renderer (for advanced usage) |
| `UniformSlot` | Renders child components from a named slot |
| `UniformText` | Renders text parameters with inline editing |
| `UniformRichText` | Renders rich text parameters with formatting |
| `DefaultNotImplementedComponent` | Fallback component for unknown types |
| `useUniformContextualEditing` | Composable for custom contextual editing setup |
| `useUniformCurrentComposition` | Access the current composition data from any descendant |
| `useUniformCurrentComponent` | Access the current component data from any descendant |
| `convertComponentToProps` | Converts a `ComponentInstance` to flattened props |
| `getParameterAttributes` | Gets editing attributes for a parameter |
| `ResolveRenderer` | Type for the component resolver function |
| `ComponentProps` | Type for component props (flattened) |

### @uniformdev/context-vue

Context and personalization component and composable exports:

| Export | Description |
| --- | --- |
| `UniformContextProvider` | Provides Context to the component tree |
| `Personalize` | Renders personalized content variations |
| `PersonalizeEdge` | Edge-mode personalization renderer |
| `Test` | Renders A/B test variations |
| `Track` | Tracks enrichment behavior on viewport visibility |
| `TrackSlot` | Tracks enrichment behavior on component load |
| `useUniformContext` | Composable: access Context instance |
| `useQuirks` | Composable: reactive quirk values |
| `useScores` | Composable: reactive score values |
| `useIsPersonalized` | Composable: whether inside a personalized variant |
| `provideUniformContext` | Manually provide Context (for custom setups) |
| `onRouteChange` | Trigger a route change event on the Context |

### @uniformdev/canvas

Core Canvas types and utilities:

| Export | Description |
| --- | --- |
| `flattenValues` | Extracts values from asset parameters |
| `ComponentInstance` | Type for a component in the composition tree |
| `RootComponentInstance` | Type for the root composition component |
| `ComponentParameter` | Type for raw parameter data |
| `AssetParamValue` | Type for asset parameter values |
| `CANVAS_DRAFT_STATE` | Constant for draft content state |
| `CANVAS_PUBLISHED_STATE` | Constant for published content state |

### @uniformdev/project-map

| Export | Description |
| --- | --- |
| `ProjectMapClient` | Client for querying project map nodes |

---

## Type definitions

### ResolveRenderer

```ts
type ResolveRenderer = (
  componentInstance: ComponentInstance
) => Component<any>;
```

### ComponentProps

```ts
type ComponentProps<TProps = unknown> = TProps & {
  component: ComponentInstance;
};
```

### UniformCompositionProps

```ts
type UniformCompositionProps = {
  /** The composition data */
  data?: RootComponentInstance;
  /** Function to resolve Vue components for Canvas component types */
  resolveRenderer?: ResolveRenderer;
  /** When to track enrichment behavior: "onView" | "onLoad" */
  behaviorTracking?: "onLoad" | "onView";
  /** Custom enhancer for contextual editing updates */
  contextualEditingEnhancer?: (
    message: Pick<UpdateCompositionMessage, "composition" | "hash">
  ) => RootComponentInstance | Promise<RootComponentInstance>;
};
```

### UniformContextProps

```ts
type UniformContextProps = {
  /** The configured Uniform Context instance */
  context: Context;
  /** Output type: "standard" (SSR/CSR evaluated) or "edge" (all variants output) */
  outputType?: "standard" | "edge";
  /** Whether to track route changes automatically */
  trackRouteOnRender?: boolean;
};
```

### UniformSlotProps

```ts
type UniformSlotProps = {
  /** Name of the slot to render */
  name?: string;
  /** Override the resolve renderer for this slot's children */
  resolveRenderer?: ResolveRenderer;
};
```

### UniformTextProps

```ts
type UniformTextProps = {
  /** The ID of the parameter */
  parameterId: string;
  /** HTML element to render (default: "span") */
  as?: string;
  /** Enable multi-line mode with white-space: pre-wrap */
  isMultiline?: boolean;
  /** Placeholder text in the Canvas editor */
  placeholder?: string | ((parameter: { id: string }) => string | undefined);
};
```

### UniformRichTextProps

```ts
type UniformRichTextProps = {
  /** The ID of the rich text parameter */
  parameterId: string;
  /** Custom resolver for rich text node renderers */
  resolveRichTextRenderer?: RenderRichTextComponentResolver;
  /** Wrapper HTML element */
  as?: keyof HTMLElementTagNameMap;
  /** Placeholder text in the Canvas editor */
  placeholder?: string | ((parameter: { id: string }) => string | undefined);
};
```

---

## Recipes

### Recipe: Adding Uniform to an existing Nuxt project

1. Install packages:

```bash
npm install @uniformdev/uniform-nuxt@latest @uniformdev/canvas@latest @uniformdev/canvas-vue@latest @uniformdev/context@latest @uniformdev/context-vue@latest @uniformdev/project-map@latest
npm install -D @uniformdev/cli@latest npm-run-all
```

2. Add environment variables to `.env`:

```bash
UNIFORM_API_KEY=your-api-key
UNIFORM_PROJECT_ID=your-project-id
```

3. Download the personalization manifest:

```bash
npx uniform context manifest download --output ./lib/uniform/contextManifest.json
```

4. Configure `nuxt.config.ts`:

```ts
import manifest from "./lib/uniform/contextManifest.json";
import type { ManifestV2 } from "@uniformdev/context";

export default defineNuxtConfig({
  modules: ["@uniformdev/uniform-nuxt"],
  uniform: {
    projectId: process.env.UNIFORM_PROJECT_ID,
    readOnlyApiKey: process.env.UNIFORM_API_KEY,
    manifest: manifest as ManifestV2,
    defaultConsent: true,
    outputType: "standard",
    playgroundPath: "/playground",
  },
});
```

5. Create the component resolver (`components/canvasComponents.ts`):

```ts
import type { ComponentInstance } from "@uniformdev/canvas";
import {
  DefaultNotImplementedComponent,
  type ResolveRenderer,
} from "@uniformdev/canvas-vue";

const componentMap: Record<string, any> = {};

export const resolveRenderer: ResolveRenderer = (component) => {
  return (
    componentMap[component.type] ?? DefaultNotImplementedComponent
  );
};
```

6. Create the catch-all route (`pages/[...path].vue`):

```vue
<script lang="ts" setup>
import { resolveRenderer } from "~/components/canvasComponents";

const route = useRoute();
const path = Array.isArray(route.params.path)
  ? `/${route.params.path.join("/")}`
  : `/${route.params.path}`;

const { composition, error } = await useUniformComposition({
  projectMapNodePath: path,
});

if (error?.value) {
  console.error("Error fetching composition", error.value);
}
</script>

<template>
  <UniformComposition
    v-if="composition"
    :data="composition"
    :resolveRenderer="resolveRenderer"
  />
</template>
```

7. Create the playground route (`pages/playground.vue`):

```vue
<script lang="ts" setup>
import { resolveRenderer } from "~/components/canvasComponents";
</script>

<template>
  <UniformPlayground :resolveRenderer="resolveRenderer" />
</template>
```

8. Create CORS middleware (`server/middleware/cors.ts`):

```ts
export default defineEventHandler((event) => {
  setResponseHeaders(event, {
    "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Expose-Headers": "*",
  });
  if (getMethod(event) === "OPTIONS") {
    event.res.statusCode = 204;
    event.res.statusMessage = "No Content.";
    return "OK";
  }
});
```

9. Add scripts to `package.json`:

```json
{
  "scripts": {
    "dev": "run-s download:manifest nuxt:dev",
    "nuxt:dev": "nuxt dev",
    "build": "run-s download:manifest nuxt:build",
    "nuxt:build": "nuxt build",
    "download:manifest": "uniform context manifest download --output ./lib/uniform/contextManifest.json",
    "uniform:push": "uniform sync push",
    "uniform:pull": "uniform sync pull",
    "uniform:publish": "uniform context manifest publish"
  }
}
```

### Recipe: Building a component with text, rich text, and slots

```vue
<!-- components/Article.vue -->
<script lang="ts" setup>
import type { ComponentInstance } from "@uniformdev/canvas";
import type { UniformRichTextNodeProps } from "@uniformdev/canvas-vue";

interface Props {
  title: string;
  subtitle?: string;
  body?: UniformRichTextNodeProps["node"];
  component: ComponentInstance;
}

defineProps<Props>();
</script>

<template>
  <article class="max-w-4xl mx-auto">
    <UniformText
      parameterId="title"
      as="h1"
      class="text-3xl font-bold mb-2"
      placeholder="Article title"
    />
    <UniformText
      parameterId="subtitle"
      as="p"
      class="text-lg text-gray-600 mb-8"
      placeholder="Article subtitle"
    />

    <div class="grid grid-cols-3 gap-8">
      <div class="col-span-2">
        <UniformRichText
          parameter-id="body"
          as="div"
          class="prose"
          placeholder="Write your article content here"
        />
      </div>
      <aside>
        <UniformSlot name="sidebar" />
      </aside>
    </div>

    <section class="mt-12">
      <h2 class="text-2xl font-semibold mb-4">Related</h2>
      <UniformSlot name="relatedContent">
        <template #default="{ child }">
          <div class="mb-4">
            <component :is="child" />
          </div>
        </template>
      </UniformSlot>
    </section>
  </article>
</template>
```

Register it in your component resolver:

```ts
import Article from "./Article.vue";

const componentMap = {
  article: Article,
  // ...other components
};
```

### Recipe: Component with quirk-based personalization

```vue
<!-- components/PersonalizedGreeting.vue -->
<script lang="ts" setup>
import { useQuirks, useScores } from "@uniformdev/context-vue";

const quirks = useQuirks();
const scores = useScores();

const topInterest = computed(() => {
  if (!scores.value) return null;
  const entries = Object.entries(scores.value);
  if (entries.length === 0) return null;
  return entries.sort(([, a], [, b]) => b - a)[0]?.[0];
});
</script>

<template>
  <div class="p-4 bg-blue-50 rounded-lg">
    <p v-if="quirks?.country">
      Welcome from {{ quirks.country }}!
    </p>
    <p v-if="topInterest">
      We see you're interested in {{ topInterest }}.
    </p>
  </div>
</template>
```

### Recipe: Forget visitor data (reset personalization)

```vue
<!-- components/ForgetMe.vue -->
<script lang="ts" setup>
const { context } = useNuxtApp().$useUniformContext();

const forgetMe = async () => {
  await context.forget(true);
};
</script>

<template>
  <button @click="forgetMe">Forget me</button>
</template>
```

### Recipe: Accessing composition data for metadata

```vue
<!-- pages/[...path].vue -->
<script lang="ts" setup>
import { resolveRenderer } from "~/components/canvasComponents";

const route = useRoute();
const path = Array.isArray(route.params.path)
  ? `/${route.params.path.join("/")}`
  : `/${route.params.path}`;

const { composition } = await useUniformComposition({
  projectMapNodePath: path,
});

// Extract metadata from composition parameters
const metaTitle = composition?.parameters?.metaTitle?.value as string;
const metaDescription = composition?.parameters?.metaDescription?.value as string;

useHead({
  title: metaTitle,
  meta: [
    { name: "description", content: metaDescription },
  ],
});
</script>

<template>
  <UniformComposition
    v-if="composition"
    :data="composition"
    :resolveRenderer="resolveRenderer"
  />
</template>
```

---

## Best practices

1. **Use TypeScript** -- The SDK is TypeScript-first. Define `Props` interfaces for all components with proper types for each parameter.
2. **Use `DefaultNotImplementedComponent` as fallback** -- Always return `DefaultNotImplementedComponent` from your `resolveRenderer` for unknown component types. It provides helpful development-time feedback about missing implementations.
3. **Download manifest before build** -- Use `run-s download:manifest nuxt:build` to ensure the manifest is always up-to-date. Never commit a stale manifest.
4. **Run `uniform:publish` after changes** -- Always run `uniform context manifest publish` after modifying signals, enrichments, or quirks. The manifest must be re-downloaded after publishing.
5. **Use `flattenValues` for assets** -- Always use `flattenValues` from `@uniformdev/canvas` for asset parameters rather than manually traversing the parameter structure.
6. **Access the `component` prop for metadata** -- When you need raw parameter data, component IDs, or slot information, use the `component` prop rather than trying to reconstruct the data from flattened props.
7. **Keep the CORS middleware in place** -- The CORS server middleware is required for visual editing in the Uniform Canvas editor. Do not remove it even if your production site doesn't need CORS.
8. **Use `useAsyncData` for data fetching** -- When fetching additional data in your components (like project map nodes for navigation), use Nuxt's `useAsyncData` to benefit from SSR data transfer and caching.
9. **Prefer `standard` output type** -- Unless you have a specific edge personalization setup, use `outputType: "standard"` for simpler and more predictable personalization behavior.
10. **Keep `resolveRenderer` lightweight** -- The resolver function is called for every component in the tree. Keep it as a simple lookup map rather than running complex logic.
