Step 4 of 10 (40% complete)
How the SDK Works
Step Code
The code for this specific step can be found on the following branch:
The @optimizely/cms-sdk is built around three core ideas: defining content types in code, registering React components against those types, and letting the SDK resolve the correct component at render time. Let's look at each.
Defining a Content Type
Every block, page, experience, and section starts with contentType(). This function does two things simultaneously:
- It generates a TypeScript type you can use in your component props
- It defines the CMS schema that gets pushed to Optimizely via the CLI
import { contentType, ContentProps } from '@optimizely/cms-sdk' export const HeroBlockContentType = contentType({ key: 'HeroBlock', displayName: 'Hero Block', baseType: '_component', properties: { title: { type: 'string', displayName: 'Title', localized: true }, subtitle: { type: 'string', displayName: 'Subtitle', localized: true }, showDecoration: { type: 'boolean', defaultValue: true }, }, }) type Props = { content: ContentProps<typeof HeroBlockContentType> } export default function HeroBlock({ content: { title, subtitle } }: Props) { return ( <section> <h1 data-epi-edit="title">{title}</h1> <p data-epi-edit="subtitle">{subtitle}</p> </section> ) }
Notice the data-epi-edit attributes on the JSX elements. These are what enable in-context editing in the Optimizely CMS preview — when an editor is viewing the page in preview mode, they can click directly on these elements to edit the content.
The baseType Values
Every content type must declare what kind of content it is:
| baseType | When to use |
|---|---|
_component | A reusable block placed inside pages |
_page | A full page type (e.g. CMSPage, StartPage) |
_experience | A Visual Builder experience |
_section | A Visual Builder section (rows and columns) |
Rendering Content Dynamically
Once components are registered (we will look at registration in the next lesson), the SDK can automatically resolve the correct component for any piece of content. You do this with OptimizelyComponent:
import { OptimizelyComponent } from '@optimizely/cms-sdk/react/server' // This renders any content type — the SDK figures out which component to use <OptimizelyComponent content={block} />
You never need to write a switch statement or a if (type === 'HeroBlock') check. The SDK reads the __typename or _metadata.types field from the content object and calls the right component automatically.
This becomes especially powerful when rendering lists of blocks:
export default function CMSPage({ content }: Props) { return ( <main> {content.blocks?.map((block, i) => ( <OptimizelyComponent key={i} content={block} /> ))} </main> ) }
A page can have any combination of blocks in any order, and this single line handles all of them.
Adding a New Block: The Full Flow
Adding a new block to the project follows a consistent five-step process:
- Create the component in
components/optimizely/block/my-block.tsxwithcontentType()and a React component - Register it in
lib/optimizely/init.tsin bothinitContentTypeRegistryandinitReactComponentRegistry - Allow it in pages by adding it to
AllBlocksContentTypesinlib/optimizely/content-types.ts - Push to CMS with
npm run opti-push - Use it — the block is now available in the Optimizely editor and renders automatically
That is the entire workflow. No GraphQL fragment updates, no manual schema changes in the CMS UI, no component mapper updates. Once you register it, the SDK handles the rest.
Official documentation: This lesson covers the most common patterns used in this starter. For more advanced cases — custom display templates, property types, Visual Builder configuration, and the full API reference — see the official Optimizely documentation: https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/docs/model-content-types
Have questions? I'm here to help!