A SvelteKit application integrated with Uniform's visual composition and personalization platform. This starter demonstrates how to build component-driven pages with visual editing capabilities and edge-side personalization.
- Overview
- Architecture
- Prerequisites
- Getting Started
- Project Structure
- Uniform SDK Integration
- Key Files Explained
- Components with Uniform SDK
- Building
This project combines:
- SvelteKit - Full-stack web framework for Svelte
- Uniform Canvas - Visual composition system for building pages from components
- Uniform Context - Personalization and A/B testing engine
- Vercel Edge Middleware - Server-side personalization at the edge
Content editors can visually compose pages in Uniform Canvas, while developers maintain full control over component implementation in Svelte.
┌─────────────────────────────────────────────────────────────────┐
│ Vercel Edge │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ middleware.ts │ │
│ │ - Initializes Uniform Context with manifest │ │
│ │ - Processes visitor signals (cookies, URL) │ │
│ │ - Handles NESI (Nested Edge-Side Includes) replacement │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SvelteKit Application │
│ │
│ ┌──────────────────┐ ┌─────────────────────────────────┐ │
│ │ hooks.server.ts │ │ +layout.svelte │ │
│ │ - Preview mode │ │ - UniformContext provider │ │
│ │ detection │ │ - Client-side context init │ │
│ └──────────────────┘ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [...path]/+page.server.ts │ │
│ │ - Fetches composition from Uniform Canvas API │ │
│ │ - Uses RouteClient for project map resolution │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [...path]/+page.svelte │ │
│ │ - UniformComposition renders the component tree │ │
│ │ - componentMap maps Uniform types → Svelte components │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Components (Hero, Card, Container, Grid, etc.) │ │
│ │ - Use UniformText/UniformRichText for inline editing │ │
│ │ - Use UniformSlot for nested component slots │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- pnpm - Use
pnpminstead ofnpmfor dependency install - NPM Token - Access to private
@uniformdev/*packages (provided by Uniform) - Node.js 22+
- Uniform Project - Admin access to create API keys
-
Set the NPM token for private package access:
export NPM_TOKEN=<your-npm-token>
-
Install dependencies:
pnpm install
-
Configure environment variables in
.env(see.env.example):UNIFORM_API_KEY= # From Uniform dashboard: Team → Security → API Key UNIFORM_PROJECT_ID= # Your Uniform project ID UNIFORM_PREVIEW_SECRET=hello-world # Secret for preview mode validation
-
Start the development server:
pnpm dev
This automatically pulls the context manifest before starting the dev server.
├── middleware.ts # Vercel Edge middleware for personalization
├── uniform.config.ts # CLI configuration for content sync
├── src/
│ ├── hooks.server.ts # SvelteKit server hooks (preview detection)
│ ├── routes/
│ │ ├── +layout.svelte # Root layout with UniformContext provider
│ │ ├── [...path]/ # Catch-all route for Uniform compositions
│ │ │ ├── +page.server.ts # Fetches composition from Canvas API
│ │ │ ├── +page.svelte # Renders UniformComposition
│ │ │ └── +error.svelte # Error boundary
│ │ ├── playground/ # Component preview page for Canvas editor
│ │ │ └── +page.svelte
│ │ └── preview/ # Preview API endpoint
│ │ └── +server.ts
│ └── lib/
│ ├── components/ # Svelte components mapped to Uniform types
│ │ ├── Card.svelte
│ │ ├── Container.svelte
│ │ ├── Grid.svelte
│ │ ├── Hero.svelte
│ │ ├── Page.svelte
│ │ └── RichText.svelte
│ └── uniform/
│ ├── componentMap.ts # Maps Uniform types → Svelte components
│ └── contextManifest.json # Downloaded personalization manifest
└── uniform-data/ # Local content sync (compositions, signals, etc.)
This project uses several Uniform SDK packages:
| Package | Purpose |
|---|---|
@uniformdev/canvas |
Core Canvas API client for fetching compositions |
@uniformdev/canvas-svelte |
Svelte components: UniformComposition, UniformSlot, UniformText, UniformRichText |
@uniformdev/canvas-sveltekit |
SvelteKit integrations: createUniformLoad, createUniformHandle, preview handler, ISR config |
@uniformdev/context |
Personalization Context engine |
@uniformdev/context-svelte |
UniformContext provider component |
@uniformdev/context-edge |
Edge-side context processing |
@uniformdev/context-edge-sveltekit |
NESI response handler for edge personalization |
@uniformdev/cli |
CLI for pulling manifests and syncing content |
Mission: Process every page request at the edge to apply personalization before the response reaches the user.
// What it does:
// 1. Creates a Uniform Context with the personalization manifest
// 2. Updates context with visitor data (cookies, URL, signals)
// 3. Fetches the page from the origin
// 4. Uses NESI handler to replace personalized placeholders in the HTMLThe middleware intercepts requests, evaluates personalization rules against visitor signals (stored in cookies), and swaps NESI placeholders in the HTML with personalized content variants. This happens at Vercel's edge network for minimal latency.
Matcher config: Excludes static files, SvelteKit internals (_app, __data.json), and Vite dev paths.
Handles SvelteKit server hooks, primarily for detecting Uniform preview/contextual editing mode:
const uniformHandle = createUniformHandle({
onPreview: (event, previewData) => {
// Called when user is in Canvas contextual editing mode
},
});Wraps the entire app with UniformContext for client-side personalization:
- Initializes the Context with the manifest
- Uses
CookieTransitionDataStorefor persisting visitor scores - Enables dev tools and debug logging in development
- Sets
outputTypeto"edge"in production for NESI output
The server-side load function that:
- Creates a
RouteClientwith API credentials - Uses
createUniformLoadto fetch the composition matching the current URL - Resolves the composition through the project map
- Returns composition data to the page
Also configures ISR (Incremental Static Regeneration) on Vercel with 60-second revalidation.
Renders the fetched composition using:
<UniformComposition data={data.data} {componentMap} matchedRoute={data.matchedRoute}>
<UniformSlot name="content" />
</UniformComposition>The componentMap maps Uniform component types to Svelte components.
Handles preview requests from Uniform Canvas for contextual editing:
- Validates preview secret
- Resolves composition paths
- Routes to the playground for pattern/component previews
A blank canvas page for previewing individual components or patterns in the Canvas editor. Receives composition data via contextual editing messages.
Maps Uniform Canvas component types to Svelte component implementations:
export const componentMap: ComponentMap = {
page: Page,
container: Container,
grid: Grid,
hero: Hero,
card: Card,
card__featured: Card, // Variant mapping
richText: RichText,
};Use the type__variant format for variant-specific mappings.
Slot components render nested child components from the composition:
| Component | Slot Name | Purpose |
|---|---|---|
Page.svelte |
content |
Main page content area |
Container.svelte |
content |
Wrapped content with max-width |
Grid.svelte |
items |
Grid item children |
These enable inline editing in Canvas contextual editing mode:
| Component | Parameter IDs | Purpose |
|---|---|---|
Hero.svelte |
title, description |
Editable hero text |
Card.svelte |
title, description |
Editable card content |
All components extend ComponentProps<T> for type-safe parameter access:
interface Props extends ComponentProps<{ title?: string; maxWidth?: string }> {}
let { title, maxWidth, component }: Props = $props();The component prop provides access to variant info (component.variant).
| Script | Description |
|---|---|
pnpm dev |
Pull manifest + start dev server |
pnpm build |
Pull manifest + production build |
pnpm pull:manifest |
Download personalization manifest from Uniform |
pnpm pull:content |
Sync content from Uniform to uniform-data/ |
pnpm push:content |
Push local content changes to Uniform |
Create a production build:
pnpm buildPreview the production build locally:
pnpm previewNote: For deployment, you may need to install a SvelteKit adapter for your target environment. This project includes
@sveltejs/adapter-vercelfor Vercel deployments.
A step-by-step technical guide for integrating Uniform Canvas and Context SDKs into a SvelteKit application.
- Node.js 18+
- pnpm (recommended) or npm
- A Uniform account with a project created
- Environment variables:
UNIFORM_API_KEY- Your Uniform API keyUNIFORM_PROJECT_ID- Your Uniform project IDUNIFORM_PROJECT_MAP_ID(optional) - Your project map IDUNIFORM_PREVIEW_SECRET(optional) - Secret for secure preview modeNPM_TOKEN(optional) - Required only for edge personalization (Step 12)
Install the required Uniform packages:
pnpm add @uniformdev/canvas @uniformdev/canvas-svelte @uniformdev/canvas-sveltekit @uniformdev/context @uniformdev/context-svelteFor edge personalization (optional but recommended for production):
pnpm add @uniformdev/context-edge @uniformdev/context-edge-sveltekitDev dependencies for CLI:
pnpm add -D @uniformdev/cliUpdate src/app.d.ts to include Uniform preview data types:
import type { UniformPreviewData } from '@uniformdev/canvas-sveltekit';
declare global {
namespace App {
interface Locals {
uniformPreview?: UniformPreviewData;
}
}
}
export {};Create src/lib/uniform/componentMap.ts to map Uniform component types to Svelte components:
import type { ComponentMap } from '@uniformdev/canvas-svelte';
// Import your Svelte components
import Hero from '$lib/components/Hero.svelte';
import Page from '$lib/components/Page.svelte';
/**
* Maps Uniform component types to Svelte components.
* The key is the component type from Uniform Canvas.
* Use `type__variant` format to map specific variants.
*/
export const componentMap: ComponentMap = {
// Page types
page: Page,
// Content components
hero: Hero,
// Variant example: card__featured uses same component
// card__featured: Card,
};Create src/hooks.server.ts to handle Uniform preview cookies:
import { sequence } from '@sveltejs/kit/hooks';
import { createUniformHandle } from '@uniformdev/canvas-sveltekit';
/**
* Uniform handle hook - parses preview cookies and attaches to locals
*/
const uniformHandle = createUniformHandle({
onPreview: (event, previewData) => {
// Optional: Log when preview mode is active
if (previewData.isUniformContextualEditing) {
console.log(
'[Uniform] Contextual editing mode active for:',
previewData.compositionPath
);
}
},
});
export const handle = sequence(uniformHandle);Update src/routes/+layout.svelte to initialize Uniform Context:
<script lang="ts">
import { dev } from '$app/environment';
import {
Context,
CookieTransitionDataStore,
enableContextDevTools,
enableDebugConsoleLogDrain
} from '@uniformdev/context';
import { UniformContext } from '@uniformdev/context-svelte';
import type { ManifestV2 } from '@uniformdev/context';
import manifestJson from '$lib/uniform/contextManifest.json';
let { children } = $props();
const context = new Context({
defaultConsent: true,
transitionStore: new CookieTransitionDataStore({
serverCookieValue: undefined,
}),
plugins: [enableContextDevTools(), enableDebugConsoleLogDrain('debug')],
manifest: manifestJson as ManifestV2,
});
</script>
<UniformContext {context} outputType={dev ? 'standard' : 'edge'}>
{@render children()}
</UniformContext>Note: The contextManifest.json file is generated by the CLI (see Step 10).
Create the route structure for serving Uniform compositions.
import { error } from '@sveltejs/kit';
import { RouteClient } from '@uniformdev/canvas';
import { env } from '$env/dynamic/private';
import type { PageServerLoad } from './$types';
/**
* Check if Uniform credentials are configured.
*/
function hasUniformCredentials(): boolean {
return Boolean(env.UNIFORM_API_KEY && env.UNIFORM_PROJECT_ID);
}
export const load: PageServerLoad = async (event) => {
if (!hasUniformCredentials()) {
error(
500,
'Uniform credentials not configured. Set UNIFORM_API_KEY and UNIFORM_PROJECT_ID in your .env file.'
);
}
// Lazy import to avoid initialization errors when credentials are missing
const { createUniformLoad } = await import('@uniformdev/canvas-sveltekit/route');
// Create RouteClient with SvelteKit env vars
const client = new RouteClient({
apiKey: env.UNIFORM_API_KEY,
projectId: env.UNIFORM_PROJECT_ID,
});
const uniformLoad = createUniformLoad({
client,
param: 'path', // Matches [...path]
});
return uniformLoad(event);
};<script lang="ts">
import { UniformComposition, UniformSlot } from '@uniformdev/canvas-svelte';
import { componentMap } from '$lib/uniform/componentMap.js';
let { data } = $props();
</script>
<UniformComposition
data={data.data}
{componentMap}
matchedRoute={data.matchedRoute}
dynamicInputs={data.dynamicInputs}
>
<UniformSlot name="content" />
</UniformComposition><script lang="ts">
import { page } from '$app/stores';
</script>
<div class="error-page">
<h1>{$page.status}</h1>
<p>{$page.error?.message || 'Page not found'}</p>
<a href="/">← Back to home</a>
</div>
<style>
.error-page {
min-height: 80vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
}
</style>Create src/routes/preview/+server.ts for Canvas contextual editing:
import { createPreviewHandler } from '@uniformdev/canvas-sveltekit/preview';
import { env } from '$env/dynamic/private';
/**
* Preview handler for Uniform Canvas contextual editing.
* Configure in Uniform Canvas: Preview URL: https://your-site.com/preview
*/
const handlers = createPreviewHandler({
secret: () => env.UNIFORM_PREVIEW_SECRET ?? '',
playgroundPath: '/playground',
resolveFullPath: ({ path, slug }) => {
return path || slug || '/';
},
});
export const GET = handlers.GET;
export const POST = handlers.POST;
export const OPTIONS = handlers.OPTIONS;Create src/routes/playground/+page.svelte for component/pattern previews:
<script lang="ts">
import { UniformComposition, UniformSlot } from '@uniformdev/canvas-svelte';
import { componentMap } from '$lib/uniform/componentMap.js';
import { EMPTY_COMPOSITION } from '@uniformdev/canvas';
</script>
<div class="playground">
<UniformComposition data={EMPTY_COMPOSITION} {componentMap}>
<UniformSlot name="content" />
</UniformComposition>
</div>
<style>
.playground {
min-height: 100vh;
padding: 2rem;
}
</style>Create components that map to your Uniform component types.
<script lang="ts">
import { UniformText, UniformRichText } from '@uniformdev/canvas-svelte';
</script>
<section class="hero">
<UniformText parameterId="title" as="h1" placeholder="Enter hero title" />
<div class="subtitle">
<UniformRichText parameterId="description" as="div" placeholder="Enter description" />
</div>
</section>
<style>
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4rem 2rem;
border-radius: 12px;
text-align: center;
}
</style><script lang="ts">
import { UniformSlot } from '@uniformdev/canvas-svelte';
import type { ComponentProps } from '@uniformdev/canvas-svelte';
interface Props extends ComponentProps<{ title?: string; description?: string }> {}
let { title, description, component }: Props = $props();
</script>
<svelte:head>
{#if title}
<title>{title}</title>
{/if}
{#if description}
<meta name="description" content={description} />
{/if}
</svelte:head>
<main class="page">
<UniformSlot name="content" />
</main>
<style>
.page {
min-height: 100vh;
padding: 2rem;
}
</style>UniformText- For editable text parametersUniformRichText- For rich text/HTML parametersUniformSlot- For nested component slotsComponentProps<T>- TypeScript interface for component props
import { uniformConfig } from '@uniformdev/cli/config';
module.exports = uniformConfig({ preset: 'all', disableEntities: ['webhook'] });{
"scripts": {
"dev": "pnpm pull:manifest && vite dev",
"build": "pnpm pull:manifest && vite build",
"pull:manifest": "uniform context manifest download --output ./src/lib/uniform/contextManifest.json"
}
}Create src/lib/uniform/contextManifest.json:
{
"project": {}
}Add to src/lib/uniform/.gitignore:
contextManifest.json
Create .env file:
UNIFORM_API_KEY=your-api-key-here
UNIFORM_PROJECT_ID=your-project-id-here
UNIFORM_PREVIEW_SECRET=your-preview-secret-hereFor production edge personalization with NESI (Nested Edge-Side Includes), you need two things:
The UniformContext in your root layout (Step 5) must use outputType="edge" in production. This is already configured if you followed Step 5:
<UniformContext {context} outputType={dev ? 'standard' : 'edge'}>
{@render children()}
</UniformContext>standard: Used in development - personalization happens client-sideedge: Used in production - outputs NESI placeholders that the edge middleware replaces
Create middleware.ts in the project root:
import {
Context,
CookieTransitionDataStore,
UNIFORM_DEFAULT_COOKIE_NAME,
} from '@uniformdev/context';
import { createUniformNesiResponseHandler } from '@uniformdev/context-edge-sveltekit';
import { next } from '@vercel/functions';
import { parse } from 'cookie';
import type { ManifestV2 } from '@uniformdev/context';
import manifestJson from './src/lib/uniform/contextManifest.json';
const manifest = manifestJson as ManifestV2;
export default async function middleware(request: Request) {
if (request.headers.get('x-from-middleware') === 'true') {
return next(request);
}
const url = new URL(request.url);
const cookieValue = request.headers.get('cookie');
const cookies = parse(cookieValue ?? '');
const context = new Context({
manifest: manifest as ManifestV2,
defaultConsent: true,
transitionStore: new CookieTransitionDataStore({
serverCookieValue: cookies[UNIFORM_DEFAULT_COOKIE_NAME] ?? undefined,
}),
});
await context.update({
cookies,
url,
});
const response = await fetch(url, {
headers: {
'x-from-middleware': 'true',
},
});
const handler = createUniformNesiResponseHandler();
const processedResponse = await handler({
response,
context,
});
return processedResponse;
}
export const config = {
matcher: [
'/((?!_app|__data\\.json|@vite|@id|@fs|_next|.*\\..*|favicon\\.ico).*)',
],
};Important: The @uniformdev/context-edge-sveltekit package is private and requires an NPM token.
Create or update .npmrc in your project root:
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
Set the NPM_TOKEN environment variable before installing:
export NPM_TOKEN=your-npm-token-hereInstall the edge packages and dependencies:
pnpm add @uniformdev/context-edge @uniformdev/context-edge-sveltekit @vercel/functions cookie- Build time: Components using personalization output NESI placeholders (e.g.,
<nesi-include src="proxy.php?url=https%3A%2F%2Fwww.github.com%2F..." />) - Edge middleware: Intercepts the response, evaluates visitor context, and replaces NESI placeholders with personalized content
- Result: Fully personalized HTML delivered from the edge with no client-side JavaScript required
Without outputType="edge" in the layout, the NESI placeholders won't be generated and the middleware will have nothing to process.
For Vercel deployments with ISR, add to your page server load:
import { createVercelIsrConfig } from '@uniformdev/canvas-sveltekit';
export const config = createVercelIsrConfig({
expiration: 60, // Revalidate every 60 seconds
});And update svelte.config.js:
import adapter from '@sveltejs/adapter-vercel';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
},
};
export default config;your-project/
├── src/
│ ├── app.d.ts # TypeScript declarations
│ ├── app.html # HTML template
│ ├── hooks.server.ts # Server hooks
│ ├── lib/
│ │ ├── components/
│ │ │ ├── Hero.svelte
│ │ │ ├── Page.svelte
│ │ │ └── index.ts
│ │ └── uniform/
│ │ ├── componentMap.ts
│ │ ├── contextManifest.json # Generated by CLI
│ │ └── .gitignore
│ └── routes/
│ ├── +layout.svelte # Root layout with UniformContext
│ ├── [...path]/
│ │ ├── +page.server.ts # Fetches compositions
│ │ ├── +page.svelte # Renders compositions
│ │ └── +error.svelte
│ ├── playground/
│ │ └── +page.svelte
│ └── preview/
│ └── +server.ts
├── middleware.ts # Edge middleware (optional)
├── uniform.config.ts # CLI configuration
├── package.json
├── svelte.config.js
├── vite.config.ts
└── .env
-
"Cannot find module contextManifest.json"
- Run
pnpm pull:manifestto download the manifest - Ensure you have valid
UNIFORM_API_KEYandUNIFORM_PROJECT_ID
- Run
-
Preview not working
- Verify preview URL is configured in Uniform Canvas settings
- Check
UNIFORM_PREVIEW_SECRETmatches in both places - If none of this helps, check this troubleshooting guide.
-
Components not rendering
- Verify component type names in
componentMapmatch Uniform Canvas - Check browser console for mapping errors
- Verify component type names in
-
TypeScript errors with slots
- Ensure
UniformSlotname matches the slot name in Uniform Canvas
- Ensure
- Create component types in Uniform Canvas
- Create a project map with nodes
- Create compositions and assign to project map nodes
- Run
pnpm devto test locally - Configure preview URL in Uniform Canvas settings