Skip to Content

How to Build Optimizely CMS 13 with Visual Builder in .NET

A code-first guide for .NET developers moving from Optimizely CMS 12 to CMS 13, covering Visual Builder: ExperienceData, Sections, Elements, tag helpers and styles.

If you have built Optimizely CMS 12 sites, your mental model runs through PageData, ContentArea, and BlockData. A page has properties, a ContentArea or two, and editors drop blocks into those areas. CMS 13 replaces this model by default. Visual Builder is now the standard editing experience, introducing a composition model built around ExperienceData, SectionData, and Elements. This is a practical guide to the new model, aimed at a developer who knows the old one and needs to write the new one correctly.

A note on sources before starting. Optimizely's own developer docs cover Visual Builder well from an editor's point of view, clicking through Add Section, Add Row, Add Column, but stay thin on C# and Razor. Most of the code below draws on hands-on write-ups from developers working against the CMS 13 pre-release build, cross-checked against the official reference material where available. Where the two disagree slightly, the disagreement is called out rather than smoothed over.

The vocabulary, mapped from what you already know

Visual Builder introduces five terms worth fixing before writing any code. An Element is a leaf node with no internal layout, the rough equivalent of a simple block. Optimizely's own docs state this plainly: "Elements do not have a layout. As an editor, you cannot divide elements or modify their structure." A Section is a horizontal slice of a page built from rows and columns, closer to a page section built from several ContentArea blocks side by side, except the layout itself now lives in structured data rather than in property definitions. An Outline is the list view of a page's sections, roughly what the All Properties tree gave you for a ContentArea, but visual and reorderable. A Blueprint is a saved section or experience layout an editor reuses, created only in the editor UI. A Style is a named, developer-defined choice, a colour scheme or a column width, offered to an editor as a dropdown and mapped to a CSS class at render time.

Defining the page type

The page type itself changes least. Instead of deriving from PageData, an Experience-enabled page type derives from EPiServer.VisualBuilder.ExperienceData, and renders through an ordinary PageController<T> exactly as a CMS 12 page type does.

[SiteContentType(GUID = "...", DisplayName = "Standard Experience", AvailableInEditMode = true)]
public class StandardExperienceData : ExperienceData
{
    [CultureSpecific]
    [Display(Order = 10, GroupName = SystemTabNames.Content)]
    public virtual string Heading { get; set; }
}
public class StandardExperienceController : PageController<StandardExperienceData>
{
    public IActionResult Index(StandardExperienceData currentPage) => View(currentPage);
}

Nothing here needs Optimizely Graph running, and nothing here breaks a routing pattern you already know.

Defining a Section type

A Section type derives from SectionData rather than BlockData.

[SiteContentType(GUID = "...", DisplayName = "Empty Section")]
public class EmptySection : SectionData
{
    [Display(Order = 10)]
    public virtual string SectionTitle { get; set; }
}

A Section optionally ships with a default layout baked in, so an editor adding one to a page gets a sensible starting grid rather than an empty shell. SetDefaultValues builds the grid out of LayoutStructureNode objects.

[ContentType(GUID = "...", DisplayName = "Two columns (wide left)", GroupName = "Sections")]
public class TwoColumnWideLeftSection : SectionData
{
    public override void SetDefaultValues(ContentType contentType)
    {
        base.SetDefaultValues(contentType);
        Layout = new Layout("grid");
        var row = new LayoutStructureNode("row");
        var left = new LayoutStructureNode("column");
        left.DisplaySettings["col"] = "col-md-9";
        var right = new LayoutStructureNode("column");
        right.DisplaySettings["col"] = "col-md-3";
        row.Nodes.Add(left);
        row.Nodes.Add(right);
        Layout.Nodes.Add(row);
    }
}

The DisplaySettings dictionary on each node carries an editor's style choice through to rendering, covered below.

Turning an existing block into an Element

This is the question most CMS 12 developers ask first, and the answer is not fully settled in the official reference yet. One documented pattern, from a developer working against the pre-release build, opts an ordinary BlockData type into Visual Builder with a single attribute value rather than a separate base class.

[SiteContentType(
    GUID = "...",
    DisplayName = "Button",
    CompositionBehaviors = [CompositionBehavior.ElementEnabledKey])]
public class ButtonBlock : BlockData
{
    public virtual string ButtonText { get; set; }
}

Optimizely's own conceptual documentation describes Elements as extending a block in a more general sense, without confirming this exact attribute as the canonical mechanism. Treat CompositionBehaviors as a working pattern worth testing against your own CMS 13 package version rather than a guaranteed stable API, and confirm the pattern against the current release notes before committing a content model to the approach.

Rendering with tag helpers

Rendering needs EPiServer.CMS.AspNetCore.TagHelpers installed, services.AddCmsTagHelpers(); registered at startup, and @addTagHelper *, EPiServer.Cms.AspNetCore.TagHelpers added to _ViewImports.cshtml. With the setup in place, a page view composes its experience with five tag helpers nested inside one another: epi-outline wraps the whole composition, epi-grid renders a section's grid, epi-row and epi-column lay out the grid, and epi-component renders whatever sits at a given node.

@model IPageViewModel<ExperienceData>
<epi-outline class="experience" experience="@Model.CurrentPage">
    <epi-grid>
        <epi-row>
            <epi-column>
                <epi-component />
            </epi-column>
        </epi-row>
    </epi-grid>
    <epi-component />
</epi-outline>

Inline-editable text still uses a tag-based property binding, close in spirit to the old @Html.PropertyFor.

<span epi-property="@Model.ButtonText">Default label</span>

A Section type gets its own view named by convention, {ContentTypeName}.cshtml, or an alternate template registered through IViewTemplateModelRegistrator when a content type needs more than one layout choice, the same pattern CMS 12 uses for named partial templates.

Wiring styles to editor choices

The DisplaySettings values set in SetDefaultValues are not only plumbing, they connect to epi-styles, which maps a named style to a set of CSS classes an editor picks from.

<epi-grid class="container">
  <epi-row class="row">
    <epi-column class="col-12">
      <epi-styles>
        <epi-style name="col">
          <epi-style-map value="col-md-9" class="col-md-9" />
          <epi-style-map value="col-md-3" class="col-md-3" />
        </epi-style>
      </epi-styles>
      <epi-component />
    </epi-column>
  </epi-row>
</epi-grid>

Registering the style options themselves, so they show up as a dropdown in the editor UI rather than a raw string field, happens in an IInitializableModule using IDisplayTemplateRepository and IContentTypeRepository to build a DisplayTemplate with DisplaySetting and DisplaySettingChoice entries. This is the part of Visual Builder with the least resemblance to anything in CMS 12: there is no equivalent of registering a dropdown of editor-facing style presets against a block type in the classic model.

Blueprints stay an editor's tool, not a developer's

One thing to unlearn from CMS 12 migration habits: a Blueprint cannot be provisioned through code or content-type definitions. Optimizely's documentation is explicit: blueprints live under a blueprints folder in the page tree and get created only through the Visual Builder UI itself, with no REST API support for blueprint creation. A developer building starter layouts for editors needs to build the starting Section and Experience types well, then hand the editor team the job of assembling and saving blueprints from inside the tool, rather than shipping blueprint content as part of a deployment.

Where CMS 12 habits cause real friction

The most common structural mistake is reaching for the old multi-area pattern, a section block with a Left ContentArea and a Right ContentArea, instead of a single SectionData type with a grid layout carrying two columns. The layout tree replaces this pattern entirely, and DisplaySettings replaces whatever bespoke metadata a CMS 12 project invented for column widths.

The second common failure mode is a style not applying visually even though the editor picked one. In practice this almost always traces back to a mismatch between the DisplaySettings key set in code and the name attribute on epi-style, an epi-style-map value not matching any DisplaySettings entry, or an epi-styles block placed outside the tag helper meant to carry the style. Checking those three in order resolves most cases.

Finally, when a layout genuinely outgrows what the tag helpers express cleanly, Optimizely's documentation does provide an escape hatch: ICompositionMapper and Html.RenderContentDataAsync() give full manual control over rendering, at the cost of writing the composition logic tag helpers would otherwise handle. The documentation recommends the tag helpers for most cases and treats the manual route as the exception, which matches how the pattern reads in practice: reach for epi-outline and friends first, and drop to the mapper only when a specific layout demands the extra control.


Code samples above are drawn from Optimizely's official CMS 13 developer documentation. Confirm API names and attribute behaviour against your installed package version before committing a content model to production

Andy Blyth

Andy Blyth, an Optimizely MVP (OMVP) and Technical Architect at MSQ DX with a keen interest in martial arts, occasionally ventures into blogging when memory serves.

optimizely-mvp-technology

SaaS CMS Cert

contentful-certified-professional

Andy Blyth