Optimizely CMS 13 is currently available as a developer preview. It introduces a more composable, headless-first architecture — and with it, several breaking changes that require hands-on code migration. This guide walks through what upgrading the Ginbok CMS backend (currently on CMS 12 + .NET 8) would look like, step by step.
⚠️ Important: CMS 13 is still in preview. Do not apply these steps to a live production site. This guide is for learning and local experimentation only. The official release guidance from Optimizely will differ.
What Changes in CMS 13?
Before touching any code, it helps to understand why things need to change. CMS 13 makes several major architectural shifts:
- .NET 10 — the minimum target framework jumps from .NET 6/8 to .NET 10.
- Application Model replaces SiteDefinition — how your site's hostname and start page are wired up is completely rethought.
- Generic content APIs —
PageReferenceandPageLinkare obsolete; everything now usesContentReferenceandContentLink. - Newtonsoft.Json removed — CMS 13 drops the dependency on Newtonsoft and migrates fully to
System.Text.Json. - Castle.Windsor removed — the old IoC container is gone; pure ASP.NET Core DI takes over entirely.
- Plugin system removed — the legacy
[EPiServerPlugIn]attribute system is completely removed. Scheduled jobs now have stricter requirements. - Dynamic Properties removed — the Dynamic Properties feature is no longer available in CMS 13.
- Mirroring removed — the content mirroring feature is removed.
Step 1: Update the Target Framework
Open your .csproj file (in Ginbok's case, the api.ginbok.com backend project) and change the target framework:
<!-- ❌ Before -->
<TargetFramework>net8.0</TargetFramework>
<!-- ✅ After -->
<TargetFramework>net10.0</TargetFramework>
You'll also need to update your SDK version in global.json if you have one pinned.
Step 2: Update NuGet Packages
Update all EPiServer.* packages to version 13.0.0-preview2. Also add a new package that CMS 13 now requires separately:
<PackageReference Include="EPiServer.CMS" Version="13.0.0-preview2" />
<PackageReference Include="EPiServer.CMS.UI" Version="13.0.0-preview2" />
<!-- ✅ New: identity management is now a separate package -->
<PackageReference Include="EPiServer.CMS.UI.AspNetIdentity" Version="13.0.0-preview2" />
Run dotnet restore and check the build output for warnings — those warnings are your migration checklist.
Step 3: Migrate from Newtonsoft.Json to System.Text.Json
CMS 13 drops the Newtonsoft.Json dependency entirely. If your project uses Newtonsoft anywhere — custom converters, serialization helpers, or JSON property attributes — you need to migrate to System.Text.Json.
Common changes:
// ❌ Before (Newtonsoft)
using Newtonsoft.Json;
[JsonProperty("my_field")]
public string MyField { get; set; }
var json = JsonConvert.SerializeObject(obj);
var result = JsonConvert.DeserializeObject<MyClass>(json);
// ✅ After (System.Text.Json)
using System.Text.Json.Serialization;
[JsonPropertyName("my_field")]
public string MyField { get; set; }
var json = JsonSerializer.Serialize(obj);
var result = JsonSerializer.Deserialize<MyClass>(json);
Note: System.Text.Json is stricter by default — it does not support reference loops, non-public setters, or some edge-case conversions that Newtonsoft handled silently. Test serialization-heavy areas carefully.
Step 4: Replace Obsolete Page APIs
CMS 13 unifies content types. PageReference and PageLink are now obsolete — every place you used them needs to be updated:
// ❌ Before
PageReference startRef = SiteDefinition.Current.StartPage;
ContentReference rootRef = currentPage.PageLink;
// ✅ After
ContentReference startRef = ContentReference.StartPage;
ContentReference rootRef = currentPage.ContentLink;
Use your IDE's "Find All References" to locate every occurrence across the solution — don't rely on runtime errors to catch these.
Step 5: Replace SiteDefinition with IApplicationResolver
This is the biggest conceptual change. SiteDefinition.Current is gone. CMS 13 introduces an Application Model — each site is an "Application" with a type (In-Process or Headless), a start page, and hostnames.
In your controllers, inject IApplicationResolver instead:
// ❌ Before
public class StartPageController : PageControllerBase<StartPage>
{
public IActionResult Index(StartPage currentPage)
{
var site = SiteDefinition.Current;
// ...
}
}
// ✅ After
public class StartPageController : PageControllerBase<StartPage>
{
private readonly IApplicationResolver _applicationResolver;
public StartPageController(IApplicationResolver applicationResolver)
{
_applicationResolver = applicationResolver;
}
public async Task<IActionResult> Index(StartPage currentPage, CancellationToken cancellationToken)
{
var application = await _applicationResolver.GetByContextAsync(cancellationToken);
var website = application as Website;
// use website.RoutingEntryPoint instead of SiteDefinition.Current.StartPage
}
}
For SiteDefinition.Current.RootPage, simply replace it with ContentReference.RootPage — that still works unchanged.
Step 6: Modernize Dependency Injection
If you're using context.Locate.Advanced.GetInstance<T>() anywhere (common in initialization modules or old service locator patterns), replace it with proper constructor injection. Castle.Windsor is also completely removed, so any Windsor-specific registration code must be replaced with ASP.NET Core DI equivalents:
// ❌ Before
var repo = context.Locate.Advanced.GetInstance<IContentRepository>();
// ✅ After — inject IServiceProvider and use:
var repo = serviceProvider.GetRequiredService<IContentRepository>();
Step 7: Replace the Legacy Plugin System
The [EPiServerPlugIn] attribute-based plugin system is completely removed in CMS 13. If you have any scheduled jobs or plugins using this pattern, they must be updated.
For scheduled jobs, the new approach requires implementing IScheduledJob and registering via DI:
// ❌ Before — legacy plugin attribute
[EPiServerPlugIn]
[ScheduledPlugIn(DisplayName = "My Job")]
public class MyScheduledJob : JobBase
{
public override string Execute() { ... }
}
// ✅ After — register as a typed scheduled job
[ScheduledPlugIn(DisplayName = "My Job", GUID = "your-guid-here")]
public class MyScheduledJob : ScheduledJobBase
{
public override string Execute() { ... }
}
// Register in Program.cs:
services.AddTransient<MyScheduledJob>();
Step 8: Update Startup.cs Configuration
Two required additions in Startup.cs / Program.cs:
// 1. Allow the DB compatibility level to auto-update
services.Configure<DataAccessOptions>(options =>
{
options.UpdateDatabaseCompatibilityLevel = true;
});
// 2. Required in preview — fixes broken menu rendering
services.AddVisitorGroups();
Also add Content Graph credentials to appsettings.json. In the preview, Content Graph is enabled by default and cannot be disabled — you must supply valid keys:
"Optimizely": {
"ContentGraph": {
"GatewayAddress": "https://staging.cg.optimizely.com",
"AllowSendingLog": "true",
"SingleKey": "YOUR_SINGLE_KEY",
"AppKey": "YOUR_APP_KEY",
"Secret": "YOUR_SECRET"
}
}
Step 9: Rebuild the Application in CMS Admin
After upgrading, run the app and you'll likely see a 404. This is expected — the database still has the old SiteDefinition. Fix it through the admin UI:
- Go to
/Optimizely/CMS→ Settings → Applications - Delete the default Headless application that was auto-created
- Click Create New Application:
- Type: In Process
- Start page: your existing Ginbok start page
- Edit the new app → Hosts → add
localhost:5000as default
Your site should now render correctly on the frontend.
What About the Next.js Frontend?
The good news: Ginbok's Next.js frontend communicates with the CMS via the Content Delivery API — which remains stable across CMS 12 → 13. Your frontend components, API routes, and content fetching logic should not need changes for the initial upgrade. The bigger impact would come if you later adopt CMS 13's native headless features.
Third-Party Package Compatibility Warning
Before upgrading, check compatibility for any third-party packages your project uses. Several known incompatibilities exist in the current preview:
- Optimizely Find — not yet compatible with CMS 13. If your site relies on Find for search, the upgrade must wait for an updated Find package.
- Optimizely Forms — known incompatibilities in the preview. Check the official release notes before upgrading.
- Geta packages (e.g., Geta.NotFoundHandler, Geta.Categories) — compatibility varies; verify each package individually against the CMS 13 preview.
- Other community/third-party extensions — any package that depends on the plugin system, Castle.Windsor, Newtonsoft.Json, or SiteDefinition APIs will need to be updated by its maintainer.
Check the official compatibility list before starting your upgrade.
Summary Checklist
- ✅ Bump target framework to
net10.0 - ✅ Update all EPiServer packages to
13.0.0-preview2 - ✅ Add
EPiServer.CMS.UI.AspNetIdentitypackage - ✅ Migrate from
Newtonsoft.JsontoSystem.Text.Json - ✅ Remove Castle.Windsor; use ASP.NET Core DI throughout
- ✅ Replace
PageReference→ContentReferenceeverywhere - ✅ Replace
PageLink→ContentLinkeverywhere - ✅ Refactor controllers to use
IApplicationResolver - ✅ Replace service locator calls with constructor injection
- ✅ Replace legacy
[EPiServerPlugIn]with new scheduled job pattern - ✅ Remove any Dynamic Properties and Mirroring usage
- ✅ Add
UpdateDatabaseCompatibilityLevelandAddVisitorGroups() - ✅ Add Content Graph credentials to
appsettings.json - ✅ Verify third-party package compatibility (Find, Forms, Geta)
- ✅ Rebuild Application via CMS Admin UI
Sources: Robert Svallin — From 12 to 13: A Developer's Guide · Optimizely Official Breaking Changes Docs