Work with Experiences
Create, render, and manage Visual Experiences, Sections, and Elements in Optimizely CMS (SaaS).
Experiences are a powerful content type in Optimizely CMS (SaaS) that let you build flexible, visual pages. Unlike traditional page (_page) types with fixed layouts, experiences (_experience) are routable entry points. They 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, ContentProps } 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, use the OptimizelyComposition component, which handles the dynamic composition structure.
import {
ComponentContainerProps,
getPreviewUtils,
OptimizelyComponent,
OptimizelyComposition,
} from '@optimizely/cms-sdk/react/server';
type Props = {
content: ContentProps<typeof AboutExperienceContentType>;
};
function ComponentWrapper({ children, node }: ComponentContainerProps) {
const { pa } = getPreviewUtils(node);
return <div {...pa(node)}>{children}</div>;
}
export default function AboutExperience({ content }: Props) {
const { pa } = getPreviewUtils(content);
return (
<main className="about-experience">
<header className="about-header">
<h1 {...pa('title')}>{content.title}</h1>
<p {...pa('subtitle')}>{content.subtitle}</p>
</header>
{content.section && (
<div className="about-section" {...pa('section')}>
<OptimizelyComponent content={content.section} />
</div>
)}
<OptimizelyComposition
nodes={content.composition.nodes ?? []}
ComponentWrapper={ComponentWrapper}
/>
</main>
);
}Key points
content.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.<OptimizelyComposition/>– This component recursively renders the entire composition structure, handling both structural nodes (rows and 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 JavaScript SDK provides BlankExperienceContentType, a ready-to-use experience type with no predefined properties. It is perfect for creating flexible pages where you build the entire layout visually.
import { BlankExperienceContentType, ContentProps } from '@optimizely/cms-sdk';
import {
ComponentContainerProps,
OptimizelyComposition,
getPreviewUtils,
} from '@optimizely/cms-sdk/react/server';
type Props = {
content: ContentProps<typeof BlankExperienceContentType>;
};
function ComponentWrapper({ children, node }: ComponentContainerProps) {
const { pa } = getPreviewUtils(node);
return <div {...pa(node)}>{children}</div>;
}
export default function BlankExperience({ content }: Props) {
return (
<main className="blank-experience">
<OptimizelyComposition
nodes={content.composition.nodes ?? []}
ComponentWrapper={ComponentWrapper}
/>
</main>
);
}Because BlankExperienceContentType has no custom properties, the visual composition interface manages the entire page layout. This gives editors maximum flexibility.
NoteThe experience uses the outline layout type. This means the Visual Builder arranges sections and section-enabled components as a flat, ordered list.
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, ContentProps } 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 the grid layout (rows and columns) manages their content (elements).
Use BlankSection
BlankSectionThe JavaScript SDK provides BlankSectionContentType for creating generic section containers. The following shows how to render a section with the OptimizelyGridSection component:
import { BlankSectionContentType, ContentProps } from '@optimizely/cms-sdk';
import {
OptimizelyGridSection,
StructureContainerProps,
getPreviewUtils,
} from '@optimizely/cms-sdk/react/server';
type BlankSectionProps = {
content: ContentProps<typeof BlankSectionContentType>;
};
export default function BlankSection({ content }: BlankSectionProps) {
const { pa } = getPreviewUtils(content);
return (
<section {...pa(content)}>
<OptimizelyGridSection nodes={content.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 render
You can customize how custom container components render rows and columns.
import { BlankSectionContentType, ContentProps } from '@optimizely/cms-sdk';
import {
OptimizelyGridSection,
StructureContainerProps,
getPreviewUtils,
} from '@optimizely/cms-sdk/react/server';
type BlankSectionProps = {
content: ContentProps<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({ content }: BlankSectionProps) {
const { pa } = getPreviewUtils(content);
return (
<section {...pa(content)}>
<OptimizelyGridSection
nodes={content.nodes}
row={CustomRow}
column={CustomColumn}
/>
</section>
);
}The row and column props accept StructureContainerProps, which provides the following:
children– The nested content to render.node– The structure node with metadata likekey,displayTemplateKey, anddisplaySettings.
This lets you apply custom styling, add CSS classes, or implement responsive grid layouts based on your design system.
Enable components for sections
To use a component 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'– Lets you use the component as a section container with grid layout capabilities.'elementEnabled'– Lets you use the component 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 you can use as an element, see the following code example:
export const CallToActionContentType = contentType({
key: "CallToAction",
baseType: "_component",
displayName: "Call to Action",
properties: {
heading: { type: "string" },
buttonText: { type: "string" },
buttonLink: { type: "string" },
},
compositionBehaviors: ["elementEnabled"],
});You can drag this component into sections within an experience as a content element.
Register experience components
Register your experience components in your application configuration.
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
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, letting editors click components and jump to the corresponding CMS field.
Mix static and composed content
Experiences can combine static properties (you define 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({ content }: Props) {
const { pa } = getPreviewUtils(content);
return (
<main>
{/* Static content from experience properties */}
<header className="about-header">
<h1 {...pa('title')}>{content.title}</h1>
<p {...pa('subtitle')}>{content.subtitle}</p>
</header>
{/* Dynamic visual composition */}
<OptimizelyComposition
nodes={content.composition.nodes ?? []}
ComponentWrapper={ComponentWrapper}
/>
</main>
);
}The static properties (title and subtitle) provide structured fields editors fill in. OptimizelyComposition renders the flexible sections and elements they arrange visually.
NoteUse static properties for critical content you must include, and composition areas for flexible, reorderable content blocks.
Updated 4 days ago
