Optimizely CMS 13 (Pre-release) Visual Builder

This post captures a proof of concept built on an Alloy MVC site upgraded to Optimizely CMS 13 pre-release, focused on Visual Builder, how to model the new ExperienceData and SectionData types, and how to configure editor-friendly style choices for Visual Builder elements.

Sample Alloy MVC repo for CMS 13:
https://github.com/evest/cms13-test1/tree/master

Why CMS 13 and Visual Builder are worth testing

CMS 13 introduces a more composition-oriented authoring approach via Visual Builder. Instead of only assembling pages via Content Areas, editors can build experiences from structured elements (sections + blocks) with better control over layout and presentation.

For teams coming from CMS 12, the important point is: you can still keep an MVC rendering approach for the site, while adopting Visual Builder concepts for composition.

Project setup context

This POC assumes:

  • You have an Alloy MVC site running on CMS 13 pre-release packages.
  • You can create and edit content in the CMS.
  • You want Visual Builder-enabled building blocks (sections + blocks) and style presets that editors can select.

1) Create an Experience content type (ExperienceData)

Example: StandardExperienceData : ExperienceData

using EPiServer.VisualBuilder;
using System.ComponentModel.DataAnnotations;
namespace Alloy13.Models.Pages;
[SiteContentType(
GUID = "3727BA9E-861F-462F-94DC-1F25F9CC3843",
DisplayName = "Standard Experience",
AvailableInEditMode = true)]
public class StandardExperienceData : ExperienceData
{
[CultureSpecific]
[Display(Order = 10, GroupName = SystemTabNames.Content)]
public virtual string Heading { get; set; }
[CultureSpecific]
[Display(Order = 20, GroupName = SystemTabNames.Content)]
public virtual string Intro { get; set; }
}

Notes

  • ExperienceData is the “host” for Visual Builder composition.
  • You can model properties similarly to Alloy pages (heading/intro/meta/settings), but the key difference is: this type is intended to be composed visually using sections/elements.

Render the Experience via MVC

You can keep a familiar controller + view pattern:

using EPiServer.Web.Mvc;
using Microsoft.AspNetCore.Mvc;
namespace Alloy13.Controllers;
public class StandardExperienceDataController : PageController<StandardExperienceData>
{
public IActionResult Index(StandardExperienceData currentPage)
{
return View(currentPage);
}
}

2) Create a Section content type (SectionData)

Sections are ideal for layout-level concepts like “Hero”, “Two Column”, “CTA Row”, etc.

Example: EmptySection : SectionData

using EPiServer.VisualBuilder;
using System.ComponentModel.DataAnnotations;
namespace Alloy13.Models.Sections;
[SiteContentType(
GUID = "427FFA65-29C3-4F7D-9FFD-B9A05FD73E79",
DisplayName = "Empty Section")]
public class EmptySection : SectionData
{
[Display(Order = 10)]
public virtual string SectionTitle { get; set; }
}

Notes

  • Think of SectionData as the structural layer: it can hold configuration (layout options, spacing, background theme) and contain child elements depending on how you design it.
  • Start minimal, then evolve sections into your design system building blocks.

3) Enable a block for use in Visual Builder

Visual Builder needs to know which blocks are allowed as composition elements. Enable a block by adding:

CompositionBehaviors = [CompositionBehavior.ElementEnabledKey]

Example: ButtonBlock enabled for Visual Builder

using EPiServer.VisualBuilder;
using System.ComponentModel.DataAnnotations;
using EPiServer.Core;
using EPiServer.SpecializedProperties;
namespace Alloy13.Models.Blocks;
[SiteContentType(
GUID = "426CF12F-1F01-4EA0-922F-0778314DDAF0",
DisplayName = "Button",
CompositionBehaviors = [CompositionBehavior.ElementEnabledKey])]
public class ButtonBlock : BlockData
{
[Required]
[Display(Order = 10)]
public virtual string ButtonText { get; set; }
[Required]
[Display(Order = 20)]
public virtual Url ButtonLink { get; set; }
}

That single property is what makes the block show up as a Visual Builder-compatible element.

4) Add editor-selectable style presets (Display Templates)

Let editors choose variants like:

  • Primary / Secondary / Danger
  • Solid / Outline
  • Light / Dark background
    …without creating a separate block type for every style.

A practical approach is to register display templates for the block type, with structured settings (a “select” control with choices).

Register templates in an initialization module

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using EPiServer.VisualBuilder.DisplayTemplates;
using EPiServer.DataAbstraction;
namespace Alloy13.Infrastructure;
[ModuleDependency(typeof(InitializationModule))]
public class VisualBuilderStyleInit : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var repo = context.Services.GetInstance<IDisplayTemplateRepository>();
var contentTypes = context.Services.GetInstance<IContentTypeRepository>();
var buttonContentTypeId = contentTypes.Load(typeof(Models.Blocks.ButtonBlock)).ID;
var template = new DisplayTemplate
{
Key = "buttondefault",
Name = "Default Button",
ContentTypeID = buttonContentTypeId,
IsDefault = true
};
template.Settings.Add(new DisplaySetting
{
Key = "bg",
Name = "Background",
Editor = "select",
Choices =
{
new DisplaySettingChoice { Key = "primary", Name = "Primary" },
new DisplaySettingChoice { Key = "secondary", Name = "Secondary" },
new DisplaySettingChoice { Key = "danger", Name = "Danger" },
new DisplaySettingChoice { Key = "transparent", Name = "Transparent" },
new DisplaySettingChoice { Key = "white", Name = "White" }
}
});
repo.Save(template);
}
public void Uninitialize(InitializationEngine context) { }
}

Notes

  • This gives editors a drop-down setting per element instance.
  • You can map these selections to Bootstrap classes, Tailwind classes, or your own design tokens.

5) Consume Display Settings in Razor

At render time, read the element’s display settings and translate them into CSS classes.

Example mapping to Bootstrap button variants

@using EPiServer.VisualBuilder.Compositions
@model Alloy13.Models.Blocks.ButtonBlock
@{
ViewData.TryGetCompositionRenderingNode(out var node);
var bg = "primary";
if (node?.DisplaySettings != null &&
node.DisplaySettings.TryGetValue("bg", out var v) &&
!string.IsNullOrWhiteSpace(v))
{
bg = v;
}
var css = bg switch
{
"secondary" => "btn-secondary",
"danger" => "btn-danger",
"transparent" => "btn-outline-primary",
"white" => "btn-light text-dark",
_ => "btn-primary"
};
}
<a class="btn @css" href="@Url.ContentUrl(Model.ButtonLink)">
@Model.ButtonText
</a>

Tip: keep blocks editable in Visual Builder

If you’re using inline editing conventions, CMS 13 supports the epi-property attribute patterns commonly used in Razor views:

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

Leave a comment