Skip to content
Optimizely

Optimizely CMS 12 Architecture

Optimizely CMS 12 represents the platform’s migration from .NET Framework to .NET Core, fundamentally modernizing the architecture. Launched in 2021 as the first version built on .NET Core (now .NET 8 in current releases), this was the most significant architectural change in the platform’s history: moving from .NET Framework 4.8 to ASP.NET Core required extensive refactoring but delivered substantial performance improvements.

Important naming context: Optimizely CMS was formerly known as Episerver CMS (rebranded in 2021). When searching for solutions, you’ll find content under both names.

  • Current Version: CMS 12 on .NET 8 (stable, production-ready)
  • Performance: Up to 3x faster than CMS 11 (measured in page rendering time)
  • Breaking Change Scope: CMS 11 to CMS 12 = major (rebuild often recommended)
  • Hosting: Azure, AWS, Linux containers, on-premises (cross-platform .NET Core)
  • DXP: Optimizely-managed Azure PaaS option

CMS 13 entered preview in Q1 2026 (.NET 10, GA expected March 31, 2026). Unlike CMS 11 to 12, the CMS 12 to 13 upgrade is lighter — primarily a framework bump from .NET 8 to .NET 10, with API modernizations but no fundamental architectural overhaul.

Key difference: CMS 13 enables Content Graph (GraphQL headless delivery) by default and cannot be disabled. This requires Content Graph credentials even for traditional MVC projects.

Recommendation for 2026: Use CMS 12 for new projects. CMS 13 is preview-only until GA (target: March 31, 2026).

Optimizely uses code-first content modeling: content types are defined as C# classes, not in a UI. This approach appeals to developers who prefer version-controlled, strongly-typed content schemas.

// Pages (routable content with URLs)
public abstract class PageData : ContentData { }
// Blocks (reusable components, no URLs)
public abstract class BlockData : ContentData { }
// Media (images, documents, videos)
public abstract class MediaData : ContentData { }

Critical rule: All content properties must be public and virtual. Optimizely uses dynamic proxies for change tracking and lazy loading. Non-virtual properties won’t be persisted.

Content Modeling: PageData, BlockData, ContentArea

Section titled “Content Modeling: PageData, BlockData, ContentArea”

Example Page Type:

[ContentType(
DisplayName = "Article Page",
GUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
Description = "Standard article page with hero, body, and related content")]
[AvailableContentTypes(
Availability.Specific,
Include = new[] { typeof(ArticlePage), typeof(CategoryPage) })]
public class ArticlePage : SitePageData
{
[Display(Name = "Hero Image", GroupName = "Content", Order = 10)]
[UIHint(UIHint.Image)]
public virtual ContentReference HeroImage { get; set; }
[Display(Name = "Main Body", GroupName = "Content", Order = 20)]
[CultureSpecific]
public virtual XhtmlString MainBody { get; set; }
[Display(Name = "Related Articles", GroupName = "Content", Order = 30)]
[AllowedTypes(typeof(ArticlePage))]
public virtual ContentArea RelatedArticles { get; set; }
[Display(Name = "Author", GroupName = "Metadata", Order = 100)]
public virtual string Author { get; set; }
[Display(Name = "Publish Date", GroupName = "Metadata", Order = 110)]
public virtual DateTime PublishDate { get; set; }
}

Key Property Types:

  • ContentReference: Reference to content (pages, blocks, media)
  • ContentArea: Container for multiple blocks (flexible layout regions)
  • XhtmlString: Rich text editor field
  • [CultureSpecific]: Property varies per language
  • [AllowedTypes]: Restricts which content types can be added

Example Block Type:

[ContentType(DisplayName = "Hero Block", GUID = "...")]
public class HeroBlock : BlockData
{
[Display(Name = "Heading")]
[Required]
public virtual string Heading { get; set; }
[Display(Name = "Subheading")]
public virtual string Subheading { get; set; }
[Display(Name = "Background Image")]
[UIHint(UIHint.Image)]
public virtual ContentReference BackgroundImage { get; set; }
[Display(Name = "CTA Text")]
public virtual string CtaText { get; set; }
[Display(Name = "CTA Link")]
public virtual Url CtaLink { get; set; }
}

Performance Pattern: Avoid Deep ContentArea Nesting

Section titled “Performance Pattern: Avoid Deep ContentArea Nesting”

ContentAreas can nest (blocks within blocks). Deep nesting causes N+1 query problems (each block loads separately).

Workaround: Bulk Loading

// BAD: Lazy loading (N+1 queries)
foreach (var item in mainContentArea.FilteredItems)
{
var block = _contentLoader.Get<IContent>(item.ContentLink);
// Process block
}
// GOOD: Bulk loading (single query)
var contentReferences = mainContentArea.FilteredItems
.Select(item => item.ContentLink)
.ToList();
var blocks = _contentLoader.GetItems(contentReferences, new LoaderOptions())
.ToList();
foreach (var block in blocks)
{
// Process block
}

CMS 12 uses .NET Core DI (not Unity like CMS 11). This is a major breaking change for CMS 11 to 12 migrations.

Example: Controller with Dependency Injection

public class ArticlePageController : PageController<ArticlePage>
{
private readonly IContentLoader _contentLoader;
private readonly IUrlResolver _urlResolver;
public ArticlePageController(
IContentLoader contentLoader,
IUrlResolver urlResolver)
{
_contentLoader = contentLoader;
_urlResolver = urlResolver;
}
public IActionResult Index(ArticlePage currentPage)
{
var model = new ArticlePageViewModel(currentPage);
return View(model);
}
}

CMS 11 Migration Note: The ServiceLocator pattern (common in CMS 11) is deprecated in CMS 12. All services must be injected via constructors.