A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Graham Carr
Jan 26, 2026
  64
(0 votes)

A day in the life of an Optimizely OMVP: Migrating an Optimizely CMS Extension from CMS 12 to CMS 13: A Developer's Guide

Migrating an Optimizely CMS Extension from CMS 12 to CMS 13: A Developer's Guide

By Graham Carr | January 2026

With Optimizely CMS 13 now available in preview, extension developers need to understand what changes are required to make their packages compatible with the new version. In this post, I'll walk through the specific changes I made to migrate OptiGraphExtensions - an Optimizely CMS add-on for managing synonyms, pinned results, webhooks, and custom data sources within Optimizely Graph.

Overview of CMS 13 Changes

Before diving into the technical details, it's worth understanding the scope of this migration. Unlike the extensive CMS 11 to CMS 12 upgrade (which involved moving from .NET Framework to .NET), the CMS 12 to CMS 13 migration is considerably more straightforward. The major changes include:

  • .NET 10 requirement - CMS 13 targets .NET 10
  • Deprecated APIs - Several legacy APIs have been marked obsolete
  • New Application Model - SiteDefinition is replaced with a more flexible application configuration
  • Shell UI Tag Helpers - New <platform-navigation /> element replaces legacy navigation helpers

Step 1: Update the Target Framework

The first and most fundamental change is updating your target framework from .NET 8 to .NET 10.

Before (CMS 12):

<TargetFramework>net8.0</TargetFramework>

After (CMS 13):

<TargetFramework>net10.0</TargetFramework>

Step 2: Update NuGet Package References

All Optimizely packages need to be updated to version 13.x. Here's what changed in the main extension project:

Before (CMS 12):

<PackageReference Include="EPiServer.CMS.UI.Core" Version="12.23.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.19" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.19" />

After (CMS 13):

<PackageReference Include="EPiServer.CMS.UI.Core" Version="13.0.0-preview2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />

For the sample CMS site, additional packages are required:

<PackageReference Include="EPiServer.CMS" Version="13.0.0-preview2" />
<PackageReference Include="EPiServer.CMS.AspNetCore.Routing" Version="13.0.0-preview2" />
<PackageReference Include="EPiServer.Cms.UI.AspNetIdentity" Version="13.0.0-preview2" />
<PackageReference Include="EPiServer.Framework" Version="13.0.0-preview2" />
<PackageReference Include="EPiServer.CMS.UI.Core" Version="13.0.0-preview2" />

Step 3: Update global.json

If your solution uses a global.json file to pin the SDK version, update it to .NET 10:

{
  "sdk": {
    "version": "10.0.102",
    "rollForward": "latestMinor"
  }
}

Step 4: Handle Deprecated APIs

CMS 13 deprecates several APIs that were commonly used in extensions. Here's what to look for and how to fix them:

PageReference → ContentReference

If your extension uses PageReference, replace it with ContentReference:

// Before
PageReference pageRef = new PageReference(123);

// After
ContentReference contentRef = new ContentReference(123);

PageData.PageLink → ContentLink

// Before
var link = pageData.PageLink;

// After
var link = pageData.ContentLink;

IContentTypeRepository Generic Parameter

The generic argument has been removed from IContentTypeRepository:

// Before
IContentTypeRepository<PageType> _pageTypeRepository;

// After
IContentTypeRepository _contentTypeRepository;

Service Location Changes

Service location via InitializationEngine.Locate and context.Locate.Advanced.GetInstance<T>() is now obsolete. Use constructor injection with IServiceProvider instead:

// Before (obsolete)
var myService = context.Locate.Advanced.GetInstance<IMyService>();

// After (preferred)
public class MyClass
{
    private readonly IMyService _myService;

    public MyClass(IServiceProvider serviceProvider)
    {
        _myService = serviceProvider.GetRequiredService<IMyService>();
    }
}

Tip: Check compiler warnings in your IDE - they will guide you to all deprecated API usage in your codebase.

Step 5: Update Startup Configuration

CMS 13 requires some additional configuration in your Startup.cs:

Add Visitor Groups Support

services.AddCmsAspNetIdentity<ApplicationUser>()
        .AddCms()
        .AddAdminUserRegistration(x => x.Behavior = RegisterAdminUserBehaviors.Enabled | RegisterAdminUserBehaviors.LocalRequestsOnly)
        .AddVisitorGroups()  // Required in CMS 13
        .AddEmbeddedLocalization<Startup>();

Configure Database Compatibility

services.Configure<DataAccessOptions>(options =>
{
    options.UpdateDatabaseCompatibilityLevel = true;
});

Step 6: Handle Blazor Component Changes (.NET 10)

If your extension uses Blazor components (as OptiGraphExtensions does), you may need to add this property to your consuming project's .csproj:

<!-- Required for .NET 10+ when Blazor components are in a referenced library -->
<RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>

This ensures that static web assets from referenced Razor Class Libraries are properly included.

Step 7: Update Admin Layout Pages with New Shell UI Tag Helpers

CMS 13 introduces a new way to integrate admin pages with the Optimizely Shell navigation using Tag Helpers. If your extension has custom admin pages, you'll need to update your layout files.

Add the EPiServer.Shell.UI Tag Helper

In your admin layout file (e.g., _LayoutBlazorAdminPage.cshtml), add the new tag helper reference:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, EPiServer.Shell.UI

The EPiServer.Shell.UI tag helper library provides new custom elements for integrating with the CMS shell.

Replace Navigation Helper Methods with Platform Navigation Element

Before (CMS 12):

@Html.CreatePlatformNavigationMenu()
<div @Html.ApplyPlatformNavigation()>
    @RenderBody()
</div>

After (CMS 13):

<platform-navigation />
<div style="padding-top: 56px; padding-left: 10px; padding-right: 10px; background: white;">
    @RenderBody()
</div>

The new <platform-navigation /> element is a Tag Helper that renders the Optimizely platform navigation bar. Key things to note:

  1. Fixed positioning - The <platform-navigation /> element creates a fixed navigation bar at the top of the page
  2. Content offset required - You must add padding-top: 56px (or similar) to your content wrapper to prevent it from being hidden behind the fixed navigation
  3. Scrolling fix - The Shell CSS may set overflow: hidden on the body, so you may need to override this:
html, body {
    overflow: auto !important;
    height: auto !important;
}

Complete Layout Example

Here's a complete example of a CMS 13 compatible admin layout:

@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Navigation
@using EPiServer.Shell.UI.Helpers.Internal

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, EPiServer.Shell.UI

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Extension</title>

    <!-- Shell resources for styling -->
    @ClientResources.RenderResources("ShellCore")
    @ClientResources.RenderResources("ShellCoreLightTheme")

    <style>
        html, body {
            overflow: auto !important;
            height: auto !important;
        }
    </style>

    <base href="~/" />
</head>
<body>
    @Html.AntiForgeryToken()
    <platform-navigation />
    <div style="padding-top: 56px; padding-left: 10px; padding-right: 10px; background: white;">
        @RenderBody()
    </div>

    <script src="_framework/blazor.server.js"></script>
</body>
</html>

Step 8: Update the Application Model (Site Configuration)

CMS 13 replaces SiteDefinition with a new Application Model. After upgrading, your site will return 404 errors until you reconfigure it:

  1. Navigate to SettingsApplications in the CMS admin
  2. Delete the default "Headless" application (if present)
  3. Create a new "In Process" application
  4. Set the start page reference
  5. Add your host entries (e.g., localhost:5000)

Code Changes for SiteDefinition

If your extension accesses site configuration programmatically:

// Before
var rootPage = SiteDefinition.Current.RootPage;

// After
private readonly IApplicationResolver _applicationResolver;

public async Task<ContentReference> GetRootPageAsync(CancellationToken cancellationToken)
{
    var app = await _applicationResolver.GetByContextAsync(cancellationToken);
    // Or use ContentReference.RootPage for the global root
    return ContentReference.RootPage;
}

Step 9: Test Thoroughly

After making all the changes:

  1. Build the solution - Fix any remaining compiler warnings about obsolete APIs
  2. Run unit tests - Ensure all tests pass with the new framework
  3. Test the admin interface - Verify your extension's UI works correctly
  4. Test all functionality - Go through each feature manually
# Build the solution
dotnet build src/OptiGraphExtensions.sln

# Run tests
dotnet test src/OptiGraphExtensions.Tests/OptiGraphExtensions.Tests.csproj

# Run the sample site
cd Sample/SampleCms
dotnet run

What Didn't Need to Change

It's worth noting what remained unchanged in the migration:

  • Entity Framework models and DbContext - Just updated package versions
  • MVC controllers and API endpoints - No changes to routing or controller structure
  • Blazor components - Component code remained the same
  • Content views - Page and block templates work the same way
  • Module configuration - The module.config format is unchanged
  • Authorization policies - Same approach works in CMS 13

Summary of Changes

Area Change Required
Target Framework net8.0net10.0
EPiServer packages 12.x13.0.0-preview2
Entity Framework 8.0.x10.0.x
PageReference Replace with ContentReference
SiteDefinition Use IApplicationResolver
Service Location Use constructor injection
Startup.cs Add AddVisitorGroups() and UpdateDatabaseCompatibilityLevel
Blazor (consuming project) Add RequiresAspNetWebAssets property
Admin Layouts Add @addTagHelper *, EPiServer.Shell.UI and use <platform-navigation />
Shell Navigation Replace @Html.CreatePlatformNavigationMenu() with <platform-navigation /> element

Conclusion

Migrating an Optimizely CMS extension from version 12 to 13 is relatively straightforward compared to previous major version upgrades. The main work involves updating package versions, replacing deprecated APIs with their modern equivalents, and testing thoroughly.

For OptiGraphExtensions, the extension's clean architecture - using dependency injection throughout and avoiding service location - meant that most of the code required no changes at all. This is a good reminder of why following modern .NET patterns pays dividends when upgrade time comes.

Resources


Note: This guide is based on the CMS 13 preview release. Some details may change before the final release. Always refer to the official Optimizely documentation for the most up-to-date information.

Graham Carr, Solutions Architect

I am an experienced Solutions Architect with over 28 years’ experience in a wide range of products and technologies. I have helped companies deliver their digital vision from concept all the way through to delivery. I have a particular passion for DXPs (Digital Experience Platforms) and am a certified developer for Optimizely as well as a Platinum OMVP.

You can also follow me on https://adayinthelife.pro

Jan 26, 2026

Comments

Please login to comment.
Latest blogs
An “empty” Optimizely CMS 13 (preview) site on .NET 10

Optimizely CMS 13 is currently available as a preview. If you want a clean sandbox on .NET 10, the fastest path today is to scaffold a CMS 12 “empt...

Pär Wissmark | Jan 26, 2026 |

Building AI-Powered Tools with Optimizely Opal - A Step-by-Step Guide

Learn how to build and integrate custom tools with Optimizely Opal using the Opal Tools SDK. This tutorial walks through creating tools, handling...

Michał Mitas | Jan 26, 2026 |

Optimizely Opal: Unlocking AI-Powered Marketing with Opal Chat & Agents

I’ve just released a new YouTube video highlighting Optimizely Opal and how it’s transforming modern marketing with AI-powered chat and intelligent...

Madhu | Jan 24, 2026 |