Skip to navigation

Get started with the Experiences SDK

Experiences SDK is currently available in alpha. APIs are unstable and subject to change.

This guide walks you from an empty Next.js project to a rendered experience. You set up the SDK first, before you have an experience or any component IDs, and add them as you go. The experience renders on the server, so your page ships only the JavaScript your components need.

The guide uses Next.js with the App Router. The SDK also has adapters for SvelteKit and Angular. See Next steps.

Prerequisites

  • Node.js 22.12 or later
  • A Contentful space with Experience Orchestration enabled
  • The Content Delivery API access token for the space: find it in Settings > API keys in the Contentful web app. To preview draft content in step 9, you also need the Content Preview API access token from the same page.

You don’t need an experience yet. Step 5 shows how to create one or seed a demo experience.

1. Create a Next.js app

npx create-next-app@15 my-experiences-app --yes --app --use-npm
cd my-experiences-app

This guide uses Next.js 15, the version the Next.js example uses.

2. Install the SDK

To install the SDK, run the following command:

npm install @contentful/experiences-react

@contentful/experiences-react is the only package you install. It includes the delivery client, the resolver, the renderers, and the types.

Add your space credentials to .env.local in the project root:

SPACE_ID=<space-id>
ENVIRONMENT_ID=<environment-id>
CDA_TOKEN=<content-delivery-api-token>

ENVIRONMENT_ID is the environment that holds your experiences, usually master.

3. Create the SDK configuration

The configuration maps the component IDs authored in Contentful to your React components. You don’t have any components registered yet, so start with an empty registry:

// lib/experience-config.ts
import type { Config } from '@contentful/experiences-react';
export const experienceConfig: Config = {
components: {},
};

You add components to components in step 6.

4. Add an experience route

Create a route that fetches an experience by ID and renders it on the server:

// app/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { fetchExperience, NotFoundError, ServerExperienceRenderer } from '@contentful/experiences-react';
import { experienceConfig } from '../../../../lib/experience-config';
export default async function ExperiencePage({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ debug?: string }>;
}) {
const { slug: experienceId } = await params;
const { debug } = await searchParams;
let experience;
try {
experience = await fetchExperience(
{
spaceId: process.env.SPACE_ID!,
environmentId: process.env.ENVIRONMENT_ID!,
experienceId,
locale: 'en-US',
},
{ accessToken: process.env.CDA_TOKEN! },
{ config: experienceConfig, debug: debug === 'true' }
);
} catch (error) {
if (error instanceof NotFoundError) notFound();
throw error;
}
return <ServerExperienceRenderer experience={experience} config={experienceConfig} />;
}

fetchExperience fetches the experience from the Experience Delivery API and resolves it. ServerExperienceRenderer renders it in a React Server Component. The experience ID comes from the URL, so you don’t need one in your code. locale selects the content locale; use one of your space’s locales. ?debug=true turns on debug mode, which you use in the next step.

An unknown experience ID throws NotFoundError, which the route turns into a 404 page.

5. Render your first experience

You need a published experience in your space. Either:

  • Create one in the Contentful web app and publish it.
  • Seed the demo experience with the bootstrap script from the Experiences SDK repository. It creates a landing experience with its components, content, and design tokens.

Start the development server:

npm run dev

Open http://localhost:3000/<experience-id>?debug=true, using the experience’s ID from Contentful, or landing for the demo.

Your registry is still empty, so each component renders as a Missing component box that names its component ID, such as Section or hero-plain, and shows the key to register. Those IDs are the keys you register in the next step. Without debug mode, unregistered components render nothing.

The boxes only show the components at the top of the experience at first. Components nested in a slot of an unregistered component appear once you register their parent. Debug mode also logs the fetch and adds a panel after the experience with the resolved experience.

6. Register your components

Write a React component for each component ID. Content properties and design properties arrive as ordinary props, under the names authored in Contentful:

// components/Heading.tsx
import type { CSSProperties } from 'react';
export interface HeadingProps {
text?: string;
// Design properties, resolved and passed as props.
as?: 'h1' | 'h2' | 'h3';
fontSize?: string;
fontWeight?: string;
}
export function Heading({ text, as = 'h2', fontSize, fontWeight }: HeadingProps) {
const Tag = as;
const style: CSSProperties = { fontSize, fontWeight };
return <Tag style={style}>{text}</Tag>;
}

Register it under its component ID:

// lib/experience-config.ts
import type { Config } from '@contentful/experiences-react';
import { Heading } from '../../../../components/Heading';
export const experienceConfig: Config = {
components: {
Heading,
},
};

Reload the page. The heading renders, and its placeholder is gone. Register the rest of the components the same way. A component’s prop names are the content and design property names of its definition in Contentful. The debug panel shows each node’s content and design keys.

A component with a slot, such as a section that contains other components, receives the slot’s rendered children as a prop named after the slot. The default slot is usually named children:

// components/Section.tsx
import type { ReactNode } from 'react';
export interface SectionProps {
children?: ReactNode[];
gap?: string;
backgroundColor?: string;
}
export function Section({ children, gap, backgroundColor }: SectionProps) {
return <section style={{ display: 'flex', flexDirection: 'column', gap, backgroundColor }}>{children}</section>;
}

The registry key is the last segment of the component’s URN in Contentful. For crn:contentful:::experience:spaces/$self/environments/$self/components/Heading, the key is Heading. Experience templates go in a separate experienceTemplates registry. See Register components.

7. Resolve design tokens

A design property can hold a design token, such as size.lg, instead of a value. Map token IDs to the values your components use with resolveToken:

// lib/experience-config.ts
import type { Config, ResolveToken } from '@contentful/experiences-react';
import { Heading } from '../../../../components/Heading';
const designTokens: Record<string, string> = {
'size.lg': '40px',
'color.primary': '#0f172a',
'fontWeight.bold': '700',
};
const resolveToken: ResolveToken = (token) => designTokens[token.value];
export const experienceConfig: Config = {
components: { Heading },
resolveToken,
};

Use the token IDs and values of your own design system. A token with no mapping reaches the component as a raw object, { type: 'DesignToken', value: '<token-id>' }, which renders as [object Object] in a style. Debug mode logs every token ID without a mapping, so you can find the ones to add. See Style components for CSS custom properties, Tailwind, and other token sources.

8. Use the SDK hooks

Most components only need their props. Two patterns cover the common exceptions.

Read design values in a nested child

A child that isn’t a registered component gets no props from the SDK. useDesignValues() reads the design values of the nearest registered component:

// components/Card.tsx
'use client';
import { useDesignValues } from '@contentful/experiences-react';
export interface CardProps {
title?: string;
ctaLabel?: string;
ctaUrl?: string;
backgroundColor?: string;
color?: string;
}
export function Card({ title, ctaLabel, ctaUrl, backgroundColor, color }: CardProps) {
return (
<article style={{ backgroundColor, color }}>
<h3>{title}</h3>
{ctaLabel && ctaUrl && <CardCta label={ctaLabel} url={ctaUrl} />}
</article>
);
}
// Not registered with the SDK, so it reads the card's design from context.
function CardCta({ label, url }: { label: string; url: string }) {
const { color } = useDesignValues<{ color?: string }>();
return (
<a href={url} style={{ background: color ?? '#111', color: '#fff' }}>
{label}
</a>
);
}

A component that calls an SDK hook, or uses state or effects, must be a client component. Add 'use client' at the top of its file. Components that only read props can stay server components.

Load data before rendering

resolveData derives props from external data, such as a price from your commerce API. It runs on the server inside fetchExperience, before the experience renders, and its return value is merged into the component’s props.

This example assumes the card has an sku content property. Add a price prop to CardProps and render it in Card, then register the card with defineComponent, keeping the components and resolveToken from the earlier steps:

// lib/experience-config.ts
import { defineComponent, type Config } from '@contentful/experiences-react';
import { Card, type CardProps } from '../../../../components/Card';
import { Heading } from '../../../../components/Heading';
import { Section } from '../../../../components/Section';
async function fetchPrice(sku: string): Promise<string | undefined> {
const response = await fetch(`https://commerce.example.com/prices/${sku}`);
if (!response.ok) return undefined;
const { price } = await response.json();
return price;
}
export const experienceConfig: Config = {
components: {
Heading,
Section,
card: defineComponent<CardProps & { price?: string }>({
component: Card,
resolveData: async ({ content }) => ({
price: await fetchPrice(content.sku as string),
}),
}),
},
resolveToken, // from step 7
};

defineComponent registers a component with resolveData or default props. The SDK runs every resolveData function in parallel. If one throws, that component renders without the resolved data, and the rest of the experience renders normally.

9. Preview draft content

To render unpublished changes, fetch from the Content Preview API. Add the Content Preview API access token to .env.local:

CPA_TOKEN=<content-preview-api-token>

Pass the token and a preview flag to fetchExperience. This example turns on preview with ?preview=true. In app/[slug]/page.tsx, read preview from the search params alongside debug, then update the fetch:

const { debug, preview } = await searchParams; // type searchParams as { debug?: string; preview?: string }
const experience = await fetchExperience(
{
spaceId: process.env.SPACE_ID!,
environmentId: process.env.ENVIRONMENT_ID!,
experienceId,
locale: 'en-US',
},
{
accessToken: process.env.CDA_TOKEN!,
previewToken: process.env.CPA_TOKEN,
preview: preview === 'true',
},
{ config: experienceConfig, debug: debug === 'true' }
);

To preview unsaved changes inside the Contentful canvas, see Set up live preview.

10. Render on the client when you need to

Server rendering is the default. Use ClientExperienceRenderer only when the experience itself must change in the browser, for example after refetching it with new parameters:

'use client';
import { ClientExperienceRenderer, type PortableRenderPlan } from '@contentful/experiences-react';
import { experienceConfig } from '../../../../lib/experience-config';
export function ExperienceClient({ experience }: { experience: PortableRenderPlan }) {
return <ClientExperienceRenderer experience={experience} config={experienceConfig} />;
}

Fetch the experience in a server component, as in step 4, and pass it to ExperienceClient as a prop. The resolved experience is plain data, so it crosses to the client.

A client component that imports experienceConfig bundles everything the configuration imports, including server-only code in resolveData. If your resolveData functions use server-only modules, give the client renderer a configuration with the same components and no resolveData.

Interactive components don’t need the client renderer. A 'use client' component hydrates inside ServerExperienceRenderer. See Render experiences.

Next steps