Work with Experience
Create, render, and manage Visual Experiences, Sections, and Elements in Optimizely CMS.
Experiences are a powerful content type in Optimizely CMS that enable flexible, visual page building. Unlike traditional page (_page) types with fixed layouts, experiences (_experience) are routable entry points that support dynamic compositions made up of sections and elements that editors can arrange and customize through the Visual Builder interface.
Create an experience content type
To create an experience, set the baseType to '_experience':
import { contentType, Infer } from '@optimizely/cms-sdk';
import { HeroContentType } from './Hero';
import { BannerContentType } from './Banner';
export const AboutExperienceContentType = contentType({
key: 'AboutExperience',
displayName: 'About Experience',
baseType: '_experience',
properties: {
title: {
type: 'string',
displayName: 'Title',
},
subtitle: {
type: 'string',
displayName: 'Subtitle',
},
section: {
type: 'content',
restrictedTypes: [HeroContentType, BannerContentType],
},
},
});The key difference from other content types is the baseType: '_experience', which automatically gives the content type access to the visual composition system.
Render an experience
To render an experience, you'll use the OptimizelyExperience component, which handles the dynamic composition structure:
import {
ComponentContainerProps,
getPreviewUtils,
OptimizelyComponent,
OptimizelyExperience,
} from '@optimizely/cms-sdk/react/server';
type Props = {
opti: Infer<typeof AboutExperienceContentType>;
};
function ComponentWrapper({ children, node }: ComponentContainerProps) {
const { pa } = getPreviewUtils(node);
return <div {...pa(node)}>{children}</div>;
}
export default function AboutExperience({ opti }: Props) {
const { pa } = getPreviewUtils(opti);
return (
<main className="about-experience">
<header className="about-header">
<h1 {...pa('title')}>{opti.title}</h1>
<p {...pa('subtitle')}>{opti.subtitle}</p>
</header>
{opti.section && (
<div className="about-section" {...pa('section')}>
<OptimizelyComponent opti={opti.section} />
</div>
)}
<OptimizelyExperience
nodes={opti.composition.nodes ?? []}
ComponentWrapper={ComponentWrapper}
/>
</main>
);
}Key points
opti.composition.nodes– Every experience has acompositionproperty that contains the visual layout structure. Thenodesarray represents all the sections and elements that editors have added to the experience.<OptimizelyExperience/>– This component recursively renders the entire composition structure, handling both structural nodes (rows, columns) and component nodes (your custom components).<ComponentWrapper/>– A wrapper function that wraps each component in the composition. This is where you add preview attributes usingpa(node)to enable on-page editing in preview mode.
Use BlankExperience
BlankExperienceThe SDK provides BlankExperienceContentType, a ready-to-use experience type with no predefined properties. It's perfect for creating flexible pages where the entire layout is built visually:
import { BlankExperienceContentType, Infer } from '@optimizely/cms-sdk';
import {
ComponentContainerProps,
OptimizelyExperience,
getPreviewUtils,
} from '@optimizely/cms-sdk/react/server';
type Props = {
opti: Infer<typeof BlankExperienceContentType>;
};
function ComponentWrapper({ children, node }: ComponentContainerProps) {
const { pa } = getPreviewUtils(node);
return <div {...pa(node)}>{children}</div>;
}
export default function BlankExperience({ opti }: Props) {
return (
<main className="blank-experience">
<OptimizelyExperience
nodes={opti.composition.nodes ?? []}
ComponentWrapper={ComponentWrapper}
/>
</main>
);
}Since BlankExperienceContentType has no custom properties, the entire page layout is managed through the visual composition interface. This gives editors maximum flexibility.
NoteThe experience uses the outline layout type, meaning sections and section-enabled components are arranged as a flat, ordered list in the Visual Builder.
Work with sections
Sections represent a vertical "chunk" of an experience and are extensions of blocks (components). A section has all the features of a block but also has access to the layout system through a composition.
Create a section content type
To create a custom section, set the baseType to '_section':
import { contentType, Infer } from '@optimizely/cms-sdk';
export const HeroSectionContentType = contentType({
key: 'HeroSection',
displayName: 'Hero Section',
baseType: '_section',
properties: {
backgroundImage: {
type: 'contentReference',
allowedTypes: ['_image'],
},
backgroundColor: {
type: 'string',
displayName: 'Background Color',
},
},
});Section content types can have properties and configuration, while their content (elements) is managed through the grid layout (rows/columns).
Use BlankSection
BlankSectionThe SDK provides BlankSectionContentType for creating generic section containers. Here's how to render a section with the OptimizelyGridSection component:
import { BlankSectionContentType, Infer } from '@optimizely/cms-sdk';
import {
OptimizelyGridSection,
StructureContainerProps,
getPreviewUtils,
} from '@optimizely/cms-sdk/react/server';
type BlankSectionProps = {
opti: Infer<typeof BlankSectionContentType>;
};
export default function BlankSection({ opti }: BlankSectionProps) {
const { pa } = getPreviewUtils(opti);
return (
<section {...pa(opti)}>
<OptimizelyGridSection nodes={opti.nodes} />
</section>
);
}<OptimizelyGridSection/>- This component renders a grid-based layout for section contents. It handles the structural organization of components within the section, including rows and columns.
Customize row and column rendering
You can customize how rows and columns are rendered by providing custom container components -
import { BlankSectionContentType, Infer } from '@optimizely/cms-sdk';
import {
OptimizelyGridSection,
StructureContainerProps,
getPreviewUtils,
} from '@optimizely/cms-sdk/react/server';
type BlankSectionProps = {
opti: Infer<typeof BlankSectionContentType>;
};
function CustomRow({ children, node }: StructureContainerProps) {
const { pa } = getPreviewUtils(node);
return (
<div className="custom-row" {...pa(node)}>
{children}
</div>
);
}
function CustomColumn({ children, node }: StructureContainerProps) {
const { pa } = getPreviewUtils(node);
return (
<div className="custom-column" {...pa(node)}>
{children}
</div>
);
}
export default function BlankSection({ opti }: BlankSectionProps) {
const { pa } = getPreviewUtils(opti);
return (
<section {...pa(opti)}>
<OptimizelyGridSection
nodes={opti.nodes}
row={CustomRow}
column={CustomColumn}
/>
</section>
);
}The row and column props accept StructureContainerProps, which provides:
children– The nested content to rendernode– The structure node with metadata likekey,displayTemplateKey, anddisplaySettings
This allows you to apply custom styling, add CSS classes, or implement responsive grid layouts based on your design system.
Enable components for sections
To allow a component to be used within experience sections, add compositionBehaviors:
export const LandingSectionContentType = contentType({
key: 'LandingSection',
baseType: '_component',
displayName: 'Landing Section',
properties: {
heading: { type: 'string' },
subtitle: { type: 'string' },
},
compositionBehaviors: ['sectionEnabled'],
});Composition behaviors:
'sectionEnabled'– Allows the component to be used as a section container with grid layout capabilities'elementEnabled'– Allows the component to be used as an element, the smallest building block with actual content data- You can specify both:
['sectionEnabled', 'elementEnabled']
Understand elements
Elements are the smallest building blocks in Visual Builder and contain the actual content data of an experience. Elements are also extensions of blocks (components) but with specific restrictions.
Create an element-enabled component
To create a component that can be used as an element:
export const CallToActionContentType = contentType({
key: 'CallToAction',
baseType: '_component',
displayName: 'Call to Action',
properties: {
heading: { type: 'string' },
buttonText: { type: 'string' },
buttonLink: { type: 'string' },
},
compositionBehaviors: ['elementEnabled'],
});This component can now be dragged into sections within an experience as a content element.
Register experience components
Don't forget to register your experience components in your application setup:
import { initContentTypeRegistry } from '@optimizely/cms-sdk';
import { initReactComponentRegistry } from '@optimizely/cms-sdk/react/server';
import AboutExperience, {
AboutExperienceContentType,
} from '@/components/AboutExperience';
import BlankExperience from '@/components/BlankExperience';
import BlankSection from '@/components/BlankSection';
// Register content types
initContentTypeRegistry([
AboutExperienceContentType,
BlankExperienceContentType,
BlankSectionContentType,
// ... other content types
]);
// Register React components
initReactComponentRegistry({
AboutExperience,
BlankExperience,
BlankSection,
// ... other components
});Best practices
Provide default wrappers
Always provide a ComponentWrapper to ensure proper preview attribute handling:
function ComponentWrapper({ children, node }: ComponentContainerProps) {
const { pa } = getPreviewUtils(node);
return <div {...pa(node)}>{children}</div>;
}This enables on-page editing in preview mode, allowing editors to click components and jump to the corresponding CMS field.
Mix static and composed content
Experiences can combine static properties (defined in your content type) with dynamic composition areas. This is useful when you need consistent elements like headers alongside flexible content:
export default function AboutExperience({ opti }: Props) {
const { pa } = getPreviewUtils(opti);
return (
<main>
{/* Static content from experience properties */}
<header className="about-header">
<h1 {...pa('title')}>{opti.title}</h1>
<p {...pa('subtitle')}>{opti.subtitle}</p>
</header>
{/* Dynamic visual composition */}
<OptimizelyExperience
nodes={opti.composition.nodes ?? []}
ComponentWrapper={ComponentWrapper}
/>
</main>
);
}The static properties (title, subtitle) provide structured fields editors fill in, while OptimizelyExperience renders the flexible sections and elements they arrange visually.
NoteUse static properties for critical content that must always be present, and composition areas for flexible, reorderable content blocks.
Updated about 22 hours ago
