Skip to navigation

Register components

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

The SDK renders an experience with your own components. You register each one under the ID it has in Contentful, in a configuration object that you pass to fetchExperience and to the renderer.

Create the configuration

The configuration has two registries and an optional token resolver:

  • components: Components, keyed by component ID.
  • experienceTemplates: Coded experience templates, keyed by experience template ID. Optional.
  • resolveToken: Resolves design tokens to values. See Style components. Optional.
// lib/experience-config.tsx
import {
defineComponent,
type Components,
type Config,
type ExperienceTemplates,
} from '@contentful/experiences-react';
import { Button } from './components/Button';
import { Heading, type HeadingProps } from './components/Heading';
import { Page } from './components/Page';
const components: Components = {
Button,
Heading: defineComponent<HeadingProps>({
defaults: { text: 'Untitled' },
component: Heading,
}),
};
const experienceTemplates: ExperienceTemplates = {
page: Page,
};
export const experienceConfig: Config = { components, experienceTemplates };

Keep the configuration in one file. Your components stay plain framework components with no SDK imports, so they work in Storybook, in tests, and in other apps. The configuration is the only place that knows about Contentful.

Choose registry keys

The registry key is the last segment of the component’s URN in Contentful. For crn:contentful:::experience:spaces/$self/environments/$self/components/Button, the key is Button. Experience template keys come from the experience template node’s URN the same way.

To find the IDs an experience uses, render it with debug mode on. Each unregistered component renders a box that names its ID. See Errors and debugging.

A component registered under an ID never satisfies an experience template with the same ID: each registry only resolves its own node type.

Register a component

Register a bare component for the common case. Use defineComponent when the component needs default props or data loaded before rendering:

FieldDescription
componentThe component to render. Required.
defaultsProps used when the experience doesn’t supply a value. They have the lowest precedence.
resolveDataA sync or async function that derives props before the experience renders.

defineComponent<Props> narrows defaults, resolveData, and component to your component’s prop type. It returns the configuration unchanged.

Load data with resolveData

resolveData runs once per fetch, inside fetchExperience or resolveExperience, before the experience renders. Its return value is merged into the component’s props. It receives:

  • content: The node’s content properties, as delivered.
  • design: The node’s design properties, as raw design values ({ type, value }).
  • experience: The render context: debug and the metadata passed to fetchExperience.
card: defineComponent<CardProps>({
resolveData: async ({ content, experience }) => {
const { badge } = await fetchCardEnrichment(content.title as string);
const locale = (experience.metadata.locale as string) ?? 'en-US';
return { badge, ctaUrl: `/${locale}${content.ctaUrl as string}` };
},
component: Card,
}),

The SDK runs every resolveData function in parallel, so a slow one doesn’t block the others. A function that throws or rejects is reported as a diagnostic, and that component renders without the resolved data.

resolveData doesn’t run again when the page re-renders or on client-side navigation. To load new data, fetch the experience again.

Merge order

A component’s props are merged in this order, last wins:

  1. defaults from defineComponent
  2. Design properties, with design tokens resolved
  3. Content properties
  4. The output of resolveData
  5. Slots

A content value always wins over a default or a design value with the same name. A default only applies when the experience doesn’t supply that property.

Render slots

Each slot arrives as a prop named after the slot, holding one entry per child node. children is the conventional name for the default slot but isn’t special: a component with a header slot receives a header prop.

A slot is a ReactNode[]. Render the array directly, or map over it to wrap or reorder children:

// components/Section.tsx
import type { ReactNode } from 'react';
export function Section({ children }: { children?: ReactNode[] }) {
return <div className="section">{children}</div>;
}

Register experience templates

A coded experience template renders the page around an experience’s content. Register it in experienceTemplates, with a bare component or defineExperienceTemplate, which takes the same fields as defineComponent. Its slots arrive as props, like a component’s: a template with a content slot receives a content prop.

// components/Page.tsx
import type { ReactNode } from 'react';
export function Page({ content }: { content?: ReactNode[] }) {
return <main>{content}</main>;
}

An experience whose template was composed in the Contentful editor has no template node: its components render at the root.

Read the raw node payload

Components receive merged props. To read a node’s original payload, call the component payload accessor from inside the component:

FieldDescription
componentIdThe component ID. Experience templates expose experienceTemplateId instead.
nodeIdThe node’s ID from the payload, when it has one. The SDK never generates IDs.
contentContent properties, as delivered.
designDesign properties, as raw design values.
resolvedThe output of resolveData, if the component has one.
attributionThe experiences and experience fragments the node belongs to. Present only when the fetch requests the content source map. See Fetch experiences.

The accessor is useContentfulComponent() in React, getContentfulComponent() in Svelte, and injectContentfulComponent() in Angular. The experience template equivalents are useContentfulExperienceTemplate(), getContentfulExperienceTemplate(), and injectContentfulExperienceTemplate(). Outside a node of that kind, React returns null and Svelte returns undefined. Angular’s accessors return a Signal, whose value is undefined outside a node.

Use the payload for a generic wrapper that branches by component ID, for analytics keyed by node ID, or for custom design handling.