
Optimizely CMS 13 First Impressions
- Mark Hall
- Optimizely
- April 2, 2026
Introduction
This week Optimizely released the long awaited CMS 13. I upgraded my Optimizely extensions package to CMS 13 and found some interesting things I needed to do to update my package. I will use this blog post to document some of those findings.
Sites deprecation in favor of applications
This change is probably the biggest change that will affect the upgrade to the newest version. When Optimizely created their SAAS platform they used the PAAS product as their base. In the SAAS world there is no notion of site becasue the web application is not hosted on the app service like the PAAS product. Those applications was created to replace sites so it could handle both use cases. This means everywhere ISiteResolver ans ISiteRepository is used will need to be updated the new APIs.
ISiteDefinitionResolver to IApplicationResolver
This is a pretty straightforward replacement. If you are still working with sites you can cast to InProcessWebsite to get access to the Hosts collection for the site. The nice thing about the new APIs are there are async methods as well.
var sitebyHostname = _applicationResolver.GetByHostname("hostname.com", false) as InProcessWebsite;
var siteByContent = _applicationResolver.GetByContent(new ContentReference()) as InProcessWebsite;
//Gets the the application from the PageHelper
var siteByContext = _applicationResolver.GetByContext() as InProcessWebsite;
ISiteDefinitionRepository to IApplicationRepository
This is a pretty straightforward replacement. If you are still working with sites you can cast to InProcessWebsite to get access to the Hosts collection for the site. The nice thing about the new APIs are there are async methods as well.
var site = _applicationRepository.Get("siteName") as InProcessWebsite;
var sites = _applicationRepository.List();
SiteDefinition.Current.StartPage
Use ContentReference.StartPage
Subscribing and Publising Events
The event system has been overhauled as well for CMS 13. In my extensions I was listening and publish events when a site had been updated. Here is how I updated to use the new sytem.
Add IEventSubscriber to Class Definition
In my extensions I was listening to the following events.
public Service(ISiteDefinitionEvents siteDefinitionEvents)
{
private readonly ISiteDefinitionEvents _siteDefinitionEvents = siteDefinitionEvents;
private void Init()
{
_siteDefinitionEvents.SiteCreated += SiteCreated;
_siteDefinitionEvents.SiteUpdated += SiteUpdated;
_siteDefinitionEvents.SiteDeleted += SiteDeleted;
}
}
We need subscribe to these in a different way in CMS 13
public Service(ISiteDefinitionEvents siteDefinitionEvents) :
IEventSubscriber<ApplicationCreatedEvent>,
IEventSubscriber<ApplicationUpdatedEvent>,
IEventSubscriber<ApplicationDeletedEvent>
{
private readonly ISiteDefinitionEvents _siteDefinitionEvents = siteDefinitionEvents;
public Task HandleAsync(ApplicationCreatedEvent eventData, EventContext context, CancellationToken cancellationToken = default)
{
if (context.Broadcasted)
{
//handle on;y broadcast events
}
return Task.CompletedTask;
}
public Task HandleAsync(ApplicationDeletedEvent eventData, EventContext context, CancellationToken cancellationToken = default)
{
if (context.Broadcasted)
{
//handle on;y broadcast events
}
return Task.CompletedTask;
}
public Task HandleAsync(ApplicationUpdatedEvent eventData, EventContext context, CancellationToken cancellationToken = default)
{
if (context.Broadcasted)
{
//handle on;y broadcast events
}
return Task.CompletedTask;
}
}
Custom Event Class
If you are creating your own event data type you will need to adjust the class
[DataContract]
[EventsServiceKnownType]
public class SettingEventData : IEventData
{
[DataMember]
public string? SiteId { get; set; }
[DataMember]
public string? ContentId { get; set; }
[DataMember]
public string? Language { get; set; }
}
Update to
[DataContract]
[EventData("b29b8aef-2a17-4432-ad16-8cd6cc6953e3", Broadcast = true)]
public class SettingEventData : IEventData
{
[DataMember]
public string? SiteId { get; set; }
[DataMember]
public string? ContentId { get; set; }
[DataMember]
public string? Language { get; set; }
}
Publish Event
Update your code to publsih events
IEventRegistry.Get(eventGuid).Raise(raiserId, data)
update to
_eventPublisher.PublishAsync(new SettingEventData
{
SiteId = id,
ContentId = e.Content.ContentGuid.ToString()
}).GetAwaiter().GetResult();
Register subsubscriptions
Add the following to your startup configuration
services.AddCmsEvents()
.AddCmsEventType<SettingEventData>()
.AddCmsEventSubscriber<ApplicationCreatedEvent, Service>()
.AddCmsEventSubscriber<ApplicationUpdatedEvent, Service>()
.AddCmsEventSubscriber<ApplicationDeletedEvent, Service>()
.AddCmsEventSubscriber<SettingEventData, Service>();
Resolving Mime Types
Instead of using IMimeTypeResolver we need to use the StaticFileOptions
//make sure a content type provider is registered in startup
services.PostConfigure<StaticFileOptions>(options => options.ContentTypeProvider ??= new FileExtensionContentTypeProvider());
//use content type prvoider
public class ExplorerController(IOptions<StaticFileOptions> options) : Controller
{
private readonly StaticFileOptions _options = options.Value;
public async Task<IActionResult> Index()
{
var file = await getFile();
var contentType = _mimeTypeResolver.ContentTypeProvider.TryGetContentType(file.Path, out var mimeType) ? mimeType : "application/octet-stream";
Response.Headers.Append("Content-Disposition", new System.Net.Mime.ContentDisposition
{
FileName = file.Name,
Inline = true,
}.ToString());
return File(file.OpenRead(), contentType);
}
}
Addon Menu Updates
When creating menu options and views for your addons, some updates are needed to show the menu. They have really simplified the process for addon developers.
Update the Layout in your views to point to the CMS UI provided one.
@using EPiServer.Shell.Navigation
@using EPiServer.Framework.Web.Resources
@using EPiServer.Framework.Localization
@using Microsoft.AspNetCore.Antiforgery;
@addTagHelper *, EPiServer.Shell.UI
@model StorageExplorerViewModel
@{
Layout = "/CmsUIViews/Views/Shared/CommonLayout.cshtml";
}
@section title
{
<title>Storage Explorer</title>
}
@section header
{
@ClientResources.RenderResources("storageexplorer", new[] { ClientResourceType.Style })
<style>
body {
margin: 0;
}
</style>
}
@section mainContent
{
<div id="root"
data-epi-antiforgery-header-name="@(Model.AntiforgeryOptions?.HeaderName ?? "")"
data-epi-antiforgery-form-field-name="@(Model.AntiforgeryOptions?.FormFieldName ?? "")"
>
</div>
@ClientResources.RenderResources("storageexplorer", new[] { ClientResourceType.Script })
}
I am senior solution architect working at Perficient helping brands create exceptional digital experiences for their customers. I am a full stack developer who enjoys the full software development lifecycle. I have expertise in Optimizely, the Azure cloud stack, React, NextJS, CI/CD pipelines, test driven development and solution architecture.
