HomeDev GuideRecipesAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

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 a composition property that contains the visual layout structure. The nodes array 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 using pa(node) to enable on-page editing in preview mode.

Use BlankExperience

The 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.

📘

Note

The 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

The 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 like key, displayTemplateKey, and displaySettings.

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.

📘

Note

Use static properties for critical content you must include, and composition areas for flexible, reorderable content blocks.