For Optimizely and .NET teams

Optimizely CMS, DXP, AI editor tools, and technical SEO

Practical solutions, open-source tools, and field-tested code to help you solve real Optimizely challenges and ship with confidence.

Latest posts

All posts →

Wednesday, 7 October 2026

Visual Builder Without Graph: Server-Side MVC Rendering in CMS 13

When teams start planning their upgrade to Optimizely CMS 13, one common misconception causes unnecessary panic: the assumption that adopting Visual Builder forces your site into a headless architecture or requires rendering every page layout through Optimizely Graph GraphQL queries.

While CMS 13 is an opinionated, Graph-first platform for search, content modeling, and headless delivery, Visual Builder does not require Graph for frontend rendering. If your site runs on traditional ASP.NET Core MVC, Optimizely CMS 13 provides a suite of native Razor tag helpers that render experiences, sections, rows, columns, and components directly on the server.

This allows you to give editors the full Visual Builder drag-and-drop experience, complete with real-time preview and interactive property highlighting, while preserving your existing MVC controllers, Razor views, and server-side performance.

On-Page Edit (OPE) in CMS 13: Disabled by Default, Not Removed

A related point of confusion is what happens to On-Page Edit (OPE). Visual Builder is the default editing interface in CMS 13, and classic OPE is disabled out of the box. However, it is not permanently gone. If your editorial team prefers the classic in-context editing view alongside Visual Builder, developers can re-enable it in Program.cs or Startup.cs:

using EPiServer.Cms.Shell.UI.Configurations;

services.Configure<CmsFeatureOptions>(options =>
{
    options.SectionsVisibility.OnPageEditing = true;
});

Even with OPE enabled, Visual Builder is the primary tool for structured layouts in CMS 13. Here is how that layout composition is structured under the hood.

Understanding the Composition Tree

Visual Builder introduces Experiences and Sections to structure layouts. Under the hood, an experience is organized as a hierarchical composition tree:

  • Experience (Root): The top-level composition page container (inheriting from ExperienceData).
  • Section (Grid): A structural block defining the layout grid.
  • Row: A horizontal layout row inside a section.
  • Column: A vertical column cell within a row.
  • Component: An individual block or element placed inside a column.

Optimizely CMS 13 maps this tree directly to five dedicated tag helpers: <epi-outline>, <epi-grid>, <epi-row>, <epi-column>, and <epi-component>.

Setup and Registration

First, verify that your project references the tag helpers package:

dotnet add package EPiServer.CMS.AspNetCore.TagHelpers

Next, register the tag helper services in your Program.cs or Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Register standard CMS dependencies
    services.AddCms();

    // Register Visual Builder tag helpers
    services.AddCmsTagHelpers();
}

Finally, expose the tag helpers to your Razor views by adding the directive to Views/_ViewImports.cshtml:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, EPiServer.Cms.AspNetCore.TagHelpers

Rendering an Experience in Razor

The simplest way to render an entire Visual Builder composition is wrapping the hierarchy in an <epi-outline> tag helper. Here is an example view for an experience page model:

@model IPageViewModel<ExperienceData>

<main class="site-main">
    <epi-outline class="experience-wrapper" experience="@Model.CurrentPage">
        <epi-grid>
            <epi-row>
                <epi-column>
                    <epi-component />
                </epi-column>
            </epi-row>
        </epi-grid>
    </epi-outline>
</main>

Here is what happens during rendering:

  • experience attribute: Accepts your ExperienceData model instance. If the model is null or contains no composition, the helper renders nothing.
  • Template resolution: For each component inside a column, the rendering engine resolves the appropriate block view using standard Optimizely MVC template resolution conventions.
  • Automatic edit attributes: In edit mode, the tag helpers automatically inject required data-epi-block-id attributes into the DOM. Editors can select, move, and edit blocks in Visual Builder without needing manual helper calls.

Customizing HTML Output Elements

By default, all composition tag helpers emit standard <div> tags. To keep your frontend semantic, use the tag-name attribute to customize the generated HTML element for each tier:

<epi-outline tag-name="section" experience="@Model.CurrentPage">
    <epi-grid tag-name="div" class="container">
        <epi-row tag-name="div" class="row">
            <epi-column tag-name="article" class="col-md-8">
                <epi-component />
            </epi-column>
            <epi-column tag-name="aside" class="col-md-4">
                <epi-component />
            </epi-column>
        </epi-row>
    </epi-grid>
</epi-outline>

Mapping Visual Builder Display Settings to CSS Classes

Visual Builder allows editors to configure display settings (such as margins, padding, or theme colors) through key-value dropdowns in the CMS UI. You can map these settings directly to CSS utility classes in Razor using the <epi-styles> tag helper:

<epi-outline experience="@Model.CurrentPage">
    <epi-styles class="layout-node">
        <epi-style name="margin" value="top" class="mt-4" />
        <epi-style-map name="spacing">
            <map value="compact" class="p-2" />
            <map value="comfortable" class="p-4" />
            <map value="spacious" class="p-6" />
        </epi-style-map>
    </epi-styles>

    <epi-grid>
        <epi-row>
            <epi-column>
                <epi-component />
            </epi-column>
        </epi-row>
    </epi-grid>
</epi-outline>

The <epi-styles> element produces no markup of its own. It acts as a configuration block instructing child composition nodes how to transform Visual Builder settings into CSS classes on the emitted HTML elements.

When to Use MVC Tag Helpers vs. Optimizely Graph

Consideration ASP.NET Core MVC Tag Helpers Optimizely Graph (GraphQL)
Frontend Architecture Coupled / Traditional ASP.NET Core Razor views Headless (Next.js, Remix, React, mobile apps)
Query Overhead None. Renders directly from in-memory content cache GraphQL network request over HTTP
Visual Builder Support Full support with live preview and edit attributes Full support via Composition model queries
Upgrade Friction Lowest risk for existing CMS 12 MVC sites Requires building or adapting a headless delivery layer

Important

  • On-Page Edit (OPE) is disabled by default in CMS 13, but can be restored using options.SectionsVisibility.OnPageEditing = true under CmsFeatureOptions.
  • While server-side MVC rendering does not require Graph for HTML generation, Graph is still a mandatory licensed component in CMS 13. Features like Content Manager search, auto-translation, and content variations still depend on Graph running in the background.
  • Tag helpers are the supported direction for coupled rendering in modern .NET. Microsoft has stopped active investment in HTML helpers (such as Html.PropertyFor), making tag helpers the recommended standard for long-term maintainability.
  • Ensure your component partial views do not make synchronous blocking database calls. The entire Visual Builder composition tree is resolved during page execution, so keep component rendering clean and async.

Links

October 07, 2026 →

Monday, 5 October 2026

Migrating from Search & Navigation (Find) to Optimizely Graph in CMS 13

If you are planning an upgrade to Optimizely CMS 13, one of the biggest architectural shifts is that Optimizely Search & Navigation (EPiServer.Find) is completely unsupported. It is not just deprecated; the packages will not run on CMS 13. Every site moving to CMS 13 must replace Find with Optimizely Graph.

The good news is that Optimizely provides a dedicated C# query package for CMS 13 (Optimizely.Graph.Cms.Query) that offers a fluent API structured very similarly to the old Find client. However, there are significant changes under the hood, particularly around package names, configuration keys, dependency injection, and a move from synchronous to asynchronous query execution.

Here is a step-by-step walkthrough to help you migrate your codebase cleanly.

Package Replacements

First, remove all legacy Find and related packages from your project files:

<!-- Remove legacy Find packages -->
<PackageReference Remove="EPiServer.Find" />
<PackageReference Remove="EPiServer.Find.Cms" />
<PackageReference Remove="EPiServer.Find.Framework" />
<PackageReference Remove="Wangkanai.Detection" />

Next, install the Optimizely Graph packages for CMS 13:

dotnet add package Optimizely.Graph.Cms.Query
dotnet add package Optimizely.Graph.AspNetCore

Optimizely.Graph.Cms.Query provides the strongly-typed LINQ-style query builder, while Optimizely.Graph.AspNetCore handles middleware and client lifecycle integration.

Configuration Changes in appsettings.json

Remove the legacy EPiServer:Find section from appsettings.json:

// Remove this block
"EPiServer": {
  "Find": {
    "ServiceUrl": "https://...",
    "DefaultIndex": "your_index_name"
  }
}

Replace it with the new Optimizely:Graph section:

"Optimizely": {
  "Graph": {
    "GatewayAddress": "https://cg.optimizely.com",
    "AppKey": "YOUR_APP_KEY",
    "Secret": "YOUR_SECRET_KEY",
    "SingleKey": "YOUR_SINGLE_KEY"
  }
}

On local environments, populate these keys from your Graph portal. In Optimizely DXP, these environment variables are provisioned automatically at deployment time.

Service Registration and Middleware

In your Startup.cs or Program.cs, swap the service registrations and middleware:

// Remove legacy Find calls:
// services.AddFind();
// services.AddDetection();
// app.UseDetection();

// Add Optimizely Graph:
services.AddContentGraph();
services.AddGraphContentClient();

// Add Graph tracking middleware
app.UseGraphTrackingScripts();

Namespace Updates

Find queries used namespaces scattered across EPiServer.Find.*. In CMS 13, the majority map directly to Optimizely.Graph.Cms.Query:

Legacy Find Namespace Optimizely Graph Replacement
EPiServer.Find Optimizely.Graph.Cms.Query
EPiServer.Find.Cms Optimizely.Graph.Cms.Query
EPiServer.Find.Api Optimizely.Graph.Cms.Query
EPiServer.Find.Framework Optimizely.Graph.Cms.Query

You can remove older references such as EPiServer.Find.Framework.Statistics and EPiServer.Find.Helpers.Text as they are no longer used.

Query Rewriting: Client and Execution Model

The single most impactful code change is how queries execute. Find allowed synchronous calls like .GetContentResult() or .GetResult(). Optimizely Graph is strictly asynchronous and relies on dependency injection rather than static singleton access.

1. Client Dependency Injection

Instead of SearchClient.Instance or injecting IClient, inject IGraphContentClient:

public class SearchPageController : PageController<SearchPage>
{
    private readonly IGraphContentClient _graphClient;

    public SearchPageController(IGraphContentClient graphClient)
    {
        _graphClient = graphClient;
    }

    // ...
}

2. Querying Content with GetAsContentAsync

In Find, fetching full content items typically looked like this:

// Legacy Find (Synchronous)
var results = SearchClient.Instance.Search<ArticlePage>()
    .For(query)
    .InField(x => x.Title)
    .Filter(x => x.Category.Match("News"))
    .Take(10)
    .GetContentResult();

In Optimizely Graph for CMS 13, use QueryContent<T>() and GetAsContentAsync():

// Optimizely Graph (Async)
var results = await _graphClient.QueryContent<ArticlePage>()
    .SearchFor(query)
    .UsingFullText()
    .Where(x => x.Category == "News")
    .Limit(10)
    .GetAsContentAsync();

GetAsContentAsync() returns an IGetAsContentResult<T> that implements IEnumerable<T>, delivering fully resolved CMS content models without requiring extra mapping layers.

3. Dynamic Filtering with BuildFilter

In Find, dynamic search criteria were typically composed by chaining .Filter() calls. In the Graph C# SDK, use BuildFilter<T>() to assemble conditional logic dynamically:

var filterBuilder = _graphClient.BuildFilter<ArticlePage>()
    .And(x => x.Name.FilterStartsWith("Tech"));

if (!string.IsNullOrEmpty(selectedAuthor))
{
    filterBuilder = filterBuilder.And(x => x.Author.Eq(selectedAuthor));
}

var results = await _graphClient.QueryContent<ArticlePage>()
    .Where(filterBuilder)
    .GetAsContentAsync();

4. Faceted Navigation

Facets work differently in Graph. When users filter by an author, you often want the facet counts for all other authors to remain visible so users can switch options easily. The Facet() extension accepts a filters parameter directly for this purpose:

var results = await _graphClient.QueryContent<ArticlePage>()
    .SearchFor(query)
    .UsingFullText()
    .Facet(x => x.Author, filters: [selectedAuthor])
    .GetAsContentAsync();

var authorFacet = results.Facets?.GetFacet(x => x.Author);

Important

  • Do not mix the platform upgrade with the search migration. Upgrade your project to CMS 13 first, verify that it compiles and starts, and then execute the Find to Graph migration on a dedicated Git branch.
  • Audit all synchronous helper methods, extensions, and custom view models. Because Graph requires await for execution, synchronous wrappers (such as .GetAwaiter().GetResult()) can cause thread pool starvation under production loads on ASP.NET Core. Convert calling methods to async up the stack.
  • Remember that CMS 13 introduces a breaking schema change in Graph compared to CMS 12. If you are already testing Graph on CMS 12, provision a separate Graph instance for your CMS 13 deployment slot to avoid corrupting your production index. Read more about this here.
  • Property indexing modes matter for query performance. Mark fields that are only displayed in results (and never searched or filtered) with the [GraphProperty(PropertyIndexingMode.OutputOnly)] attribute to minimize Graph indexing overhead.

Links

October 05, 2026 →

Friday, 2 October 2026

From robots.txt to GEO Analytics: A Year On

In September 2025, I wrote about updating your robots.txt file to explicitly allow AI bots like GPTBot, ClaudeBot, PerplexityBot, and OAI-SearchBot. Back then, the main challenge was simply granting access: deciding whether to allow training crawlers, search indexing bots, or a mix of both.

A year later, the conversation has shifted. Allowing bots in robots.txt is only table stakes. The real question today is: once you let them in, how often do they visit, what are they reading, and does that traffic convert into actual user referrals? This discipline is known as Generative Engine Optimization (GEO), and Optimizely CMS 13 now builds native tools to measure and optimize for it.

The Evolution from Access to Measurement

Allowing an AI user-agent in robots.txt tells an LLM crawler it is legally and technically welcome. What it does not tell you is whether the crawler understood your content structure, indexed your canonical answers, or simply scrapped your raw HTML without citing your brand.

Here is what the shift looks like in practice:

  • 2025 (Permission): Defining user-agent rules in robots.txt to prevent AI engines from silently bypassing your site.
  • 2026 (Visibility & Analytics): Tracking crawl frequency, measuring referral rates, and observing which specific pages AI platforms extract data from.
  • 2026 (Optimization): Structuring content specifically for AI ingestion using dedicated schema, Q&A fields, and entity models.

Tracking AI Crawlers with GEO Analytics & Agent Visibility

In Optimizely Reporting and Optimizely Analytics, the platform tracks incoming bot traffic categorized specifically by AI engine rather than burying them inside generic organic traffic numbers.

The core metric introduced here is the Crawl-to-Refer Ratio:

Metric State What It Means Action Required
High Crawl-to-Refer Ratio Bots crawl your pages frequently, but AI platforms rarely cite your site in user answers. Improve content structure, add clear Q&A summaries, and use explicit schema so AI models can easily attribute answers.
Low Crawl-to-Refer Ratio Visits are proportional to references and citations in generative responses. Identify your top-performing content templates and replicate their layout across other sections.

Note: While earlier preview documentation referred to the "GEO Analytics dashboard", newer environments direct teams to the Agent Visibility dashboard inside Optimizely Analytics. This view enriches crawler hits with request intent and Optimizely Opal / Mark facet insights.

Native AI Optimization Features in CMS 13

Beyond tracking bots, Optimizely CMS 13 introduces dedicated agents and metadata models to help content rank in generative engine answers:

1. Schema and Answers Agent

Generative search engines prefer extractable, atomic facts over sprawling multi-paragraph layouts. The Schema and Answers Agent automatically evaluates content relationships and suggests structured metadata, FAQ schemas, and direct Q&A blocks so LLMs can cleanly parse answers.

2. GEO Recommendations Agent

Similar to traditional on-page SEO checkers, this agent runs checks against your draft content in Visual Builder, evaluating your pages against known generative search visibility signals before publishing.

3. Optimizely Graph Metadata Integration

Because Optimizely Graph indexes the composition model of your pages and blocks, it surfaces structured metadata contracts (such as _itemMetadata and _assetMetadata) directly. When AI aggregators and agents query your GraphQL endpoint or rendered pages, the data schema is unambiguous.

Updating Your Strategy: What to Do Next

  1. Audit your current robots.txt: Verify that recent AI crawlers are still permitted. If you used the balanced template from last year (allowing search bots while blocking training crawlers), verify that your directives reflect current user-agent names.
  2. Review the Agent Visibility Dashboard: Check which pages have the highest crawl volume from OpenAI and Perplexity. Look for high crawl counts paired with low citation referrals.
  3. Add Structured Q&A Blocks: Update key commercial and technical documentation pages with concise summary definitions and FAQ schema markup.
  4. Leverage SEOBOOST add-on: If you are managing technical metadata, canonical tags, and robots directives on Optimizely CMS, use automated metadata tooling so headers and crawler rules stay aligned across multi-site instances.

Important

  • Do not rely entirely on robots.txt for compliance. Some rogue or emerging AI scrapers ignore standard robots directives. For strict enforcement, pair your robots.txt file with Web Application Firewall (WAF) bot rules on Optimizely DXP or Cloudflare.
  • The legacy GEO Analytics dashboard was grandfathered for early access instances. Modern CMS 13 setups use the Agent Visibility dashboard in Optimizely Analytics for richer crawler telemetry.
  • Generative search engines value freshness and canonical accuracy. If your content updates frequently, ensure your XML sitemap and Optimizely Graph index sync without delay so bots do not reference stale answers.

Links

October 02, 2026 →

Thursday, 1 October 2026

Why Your CMS 12 and CMS 13 Sites Can't Share a Graph Instance

If you're planning your Optimizely CMS 13 upgrade and you already run Optimizely Graph on your CMS 12 site, there's one detail that catches almost everyone off guard: you cannot point your CMS 13 environment at the same Graph instance your live CMS 12 site is using. Not temporarily, not just for testing. CMS 13 ships a breaking change to the Graph schema, and a CMS 12 site and a CMS 13 site cannot share a Graph instance without migrating the schema first.

This isn't an optional step you can defer. Graph is required to use CMS 13's full capabilities, including Content Manager and Experiences, so for any site already running Graph on CMS 12, migrating it is part of the CMS 13 upgrade itself, not a follow-up task.

Why This Happens

CMS 13 changes what Graph indexes and how it's shaped. Experiences, Sections, and Visual Builder's composition model change the object graph that gets synced from your CMS content tree, so the schema CMS 13 expects Graph to serve is different from the one CMS 12 populates. Point a CMS 13 site at a CMS 12 Graph instance (or vice versa) and queries either fail outright or quietly return the wrong shape of data.

That's why the upgrade guidance is blunt about it: treat the Graph migration as a required part of moving to CMS 13, not a nice-to-have cleanup step you can do later.

Two Ways to Handle the Migration

You have two supported paths. Which one you pick depends mostly on whether your site is live and how much downtime you can tolerate.

Option 1: Provision a New Graph Instance (Recommended, Zero Downtime)

Your live CMS 12 site keeps using its existing Graph instance while your CMS 13 deployment slot uses a brand new one. Graph never goes down for either version.

  1. Log in to the DXP Management Portal at https://paasportal.episerver.net.
  2. Open your organization and project, then go to the API tab and open Optimizely Graph Service Configurations. Click Prepare for CMS 13 to provision a separate Graph instance.
  3. Deploy CMS 13 through a deployment slot. The slot uses the new Graph credentials while production keeps using the old ones.
  4. Validate the slot against the CMS 13 schema without touching your live CMS 12 Graph data.
  5. Swap the slot into production. Production automatically picks up the new CMS 13 Graph credentials.

The old CMS 12 Graph instance isn't deleted immediately. It's retired in two stages so anything still pointing at the old keys has time to surface:

  • Stage 1 - Key rotation (soft delete): runs once all linked front-end environments are confirmed on the new key and a grace period (default seven days) has elapsed since that confirmation — or, if there are no linked front-end environments, seven days after go-live. This disables the old keys, so anything still using them starts failing visibly.
  • Stage 2 - Hard delete: the old Graph instance is removed a further 30 days after Stage 1.

That visible failure in Stage 1 is a feature, not a bug. It's your forcing function for finding every forgotten front end, webhook, or scheduled job that still has the old single key hardcoded somewhere.

Option 2: Smooth Rebuild on Your Existing Instance (Downtime Required)

If you'd rather keep your existing Graph instance and credentials (so external consumers never need to update anything), you can migrate the schema in place instead. This requires taking the site offline.

  1. Put the site behind a static maintenance page.
  2. In CMS, go to Settings > Smooth Rebuild, open Smooth Rebuild Settings, and click Start.
  3. Graph creates a new deployment slot (green) alongside the current one (blue) and starts syncing content into it.
  4. Test the green slot before promoting it. Use the Optimizely Graph playground (GraphiQL) with the New toggle enabled, or add the cg-query-new: true header to your GraphQL requests to target it directly.
  5. Deploy CMS 13 through a deployment slot once the rebuild has finished syncing.
  6. Promote the green slot to live.
cg-query-new: true

Because the CMS 13 schema change is breaking, you can't use Graph's normal zero-downtime read-only or read-write slot modes here. The site has to be fully offline behind the maintenance page for the duration of the rebuild.

Important

  • If you haven't implemented Optimizely Graph on your CMS 12 project at all, none of this applies to you. Follow the standard DXP upgrade path through Integration, Preproduction, and Production.
  • Run whichever path you choose in Integration and Preproduction first. You need to know how long the sync or rebuild actually takes before you size a Production maintenance window around it.
  • Audit anything with a hardcoded Graph single key (Next.js front ends, webhooks, scheduled jobs) before you start. Key rotation will break them loudly, which is useful, but you'd rather know in advance than get paged for it.
  • Local development config (GatewayAddress, AppKey, Secret, SingleKey in appsettings.json or web.config) is separate from what DXP auto-provisions on deployment. Don't carry your CMS 12 Graph configuration into your CMS 13 deployment and assume it'll just work.

Which Option Should You Pick?

Situation Go with
Site is live in production and downtime isn't acceptable Option 1: new Graph instance
Site isn't live yet, or a maintenance window is acceptable Option 2: smooth rebuild
Several external consumers have the single key hardcoded Option 1: forced key rotation surfaces every stale config for you
You want zero changes for external Graph consumers Option 2: credentials never change

Either way, don't treat this as a side detail of the CMS 13 upgrade. It's the part of the migration most likely to take down search, navigation, or personalization on your site if you discover it mid-deployment instead of during planning.

Links

October 01, 2026 →

Thursday, 13 August 2026

Optimizely XML Resource Builder: A Visual Studio Extension for Language Resource Files

Anyone who has worked on a larger Optimizely solution will know the feeling. You add or update a page type, block type, or shared base class, and then need to keep the language resource XML in sync. It is not difficult work, but it is repetitive, easy to miss, and becomes more painful as the number of content types grows.

This started as a small pet project for my own convenience. I wanted a quick way to generate a starting point for Optimizely language resource files directly from the content type code already in a project. Once it was useful in day-to-day work, I thought it could also benefit the wider Optimizely community if I packaged it properly and made it publicly available.

That is where Optimizely XML Resource Builder came from.

What it does

Optimizely XML Resource Builder is a Visual Studio extension that scans the project you select and generates an Optimizely language resource file from its C# content type definitions.

It reads content type attributes such as ContentType and SiteContentType, then creates the XML entries for the content type name, description, properties, captions, and help text.

The default output is:

Resources\Translations\ContentTypes-en.xml

You can change both the language code and output folder from the extension dialog.

Why I built it

Language resource files are useful for keeping the Optimizely editor UI clean and consistent, especially when you are working across several languages. The challenge is that the information is usually already present in code:

[ContentType(DisplayName = "Article Page")]
public class ArticlePage : SitePageData
{
    [Display(Name = "Heading", Description = "The main page heading")]
    public virtual string Heading { get; set; }
}

Copying those values into XML by hand adds another maintenance step. The extension uses the information already available in the attributes and gives you a generated file to review, commit, and translate.

Key features

  • Project-level command: right-click an Optimizely project in Solution Explorer to generate the resource XML.
  • Flexible content type detection: scan for SiteContentType, ContentType, or any additional attributes you configure.
  • Configurable generation dialog: choose a comma-separated attribute list, language code, excluded property prefixes, and output folder before generating the file.
  • Caption casing control: keep the original display-name case or convert captions to sentence case while preserving uppercase acronyms.
  • Overwrite control: choose whether to replace the existing XML file or safely add only missing entries.
  • Useful XML generated from code: create content type names, descriptions, property captions, and help text from the attributes already defined in C#.
  • Shared base-class support: find properties on custom base classes and add them under icontentdata.
  • Safe updates: add only missing XML entries by default, preserving existing translations and custom values. Enable overwrite only when you want a fresh file.
  • Solution-specific preferences: save settings per solution, rather than using one global set of values across every project.

How to use it

  1. Install the extension.
  2. Open an Optimizely solution in Visual Studio.
  3. In Solution Explorer, right-click the project that contains the content type classes.
  4. Select Build Optimizely XML Resource File.
  5. Set the attribute list, language code, output folder, and any exclusions.
  6. Select OK.

If no matching content types are found, the extension shows a warning and does not create an empty XML file.

Example output

<contenttypes>
  <icontentdata>
    <properties>
      <metadescription>
        <caption>Meta description</caption>
      </metadescription>
    </properties>
  </icontentdata>
  <articlepage>
    <name>Article Page</name>
    <properties>
      <heading>
        <caption>Heading</caption>
        <help>The main page heading</help>
      </heading>
    </properties>
  </articlepage>
</contenttypes>

Compatibility

The extension supports Visual Studio 2022 and later.

Links

I hope it saves other Optimizely developers the same repetitive work it saves me. If you try it, feedback and contributions are very welcome.

August 13, 2026 →

Tuesday, 21 July 2026

Advanced Task Manager Gets a Big Update for Optimizely CMS

Optimizely's content approval workflow works well when you are approving one item at a time. The problem starts when you are managing a large queue across pages, blocks, and media. Once the list grows, finding the right items, approving them in bulk, or publishing them in a controlled way becomes much more time-consuming than it should be.

This update focuses on solving those real editorial workflow problems. Every feature below came from day-to-day use of the tool in larger Optimizely solutions where too many clicks, too much scrolling, or missing workflow controls were slowing editors down.

Related posts: CMS 12 release post | Legacy CMS 11 post

Advanced filtering

The task list now includes a proper filter bar, making it easier to work with larger approval queues.

  • Status - switch between In Review and Ready to Publish.
  • Type - filter Pages, Blocks, or Assets/Media.
  • Site - narrow the list to one site in multi-site solutions.
  • Content search - search by content ID or part of the content name.

Each filter updates the URL, so filtered views can be bookmarked or shared with other editors.

Note: Active filters appear as removable badges below the filter bar, and a Clear all option removes everything at once.

Select all across pages

The existing table checkbox used to select only the current page of results. This update adds support for selecting all matching items across all pages.

If more results exist than are visible on the current page, a banner appears and lets the editor load every matching approval ID into the current selection. That means bulk approval can now cover the full filtered result set in one action.

This selection respects the active filters, so editors can safely approve only the tasks they intended to include.

Scheduled publishing

Publishing immediately after approval is still supported, but it is no longer the only option.

When Publish selected content after approval is enabled, editors can now choose to schedule publishing for a specific future date and time. The selected datetime is passed into Optimizely's normal publishing pipeline using IVersionable.StartPublish, so standard scheduled publishing behavior is used without any custom publishing job.

Approve blocks and media used by a page

This is one of the most useful additions in the release.

Sometimes a page is ready to go live, but the blocks or media it references are still waiting in their own approval queues. Instead of finding those items one by one, editors can now use Approve Page Dependencies to approve pending block and media dependencies for a selected page in one action.

  1. Open Approve Page Dependencies from the filter bar.
  2. Select a page from the hierarchy tree, or enter the content ID directly.
  3. Choose one or more language branches.
  4. Optionally enter an approval comment.
  5. Optionally publish approved dependencies immediately.
  6. Run the action and review the result count.

Behind the scenes, the tool inspects ContentArea and ContentReference properties on the selected page, gathers referenced blocks and media, and approves the items that still have pending approval steps.

Note: This action approves only the dependencies referenced by the page. It does not approve the page itself.

Task ordering

The task list columns are now sortable. Editors can sort by content name, content type, task type, submission date, who started the review, or deadline if the deadline property is enabled.

This sounds small, but it makes a big difference when triaging long queues and trying to prioritize urgent work.

Site column for multi-site solutions

In multi-site installations, the list now includes a Site column so editors can immediately see which site each task belongs to. In single-site environments, the column stays hidden.

This works especially well with the Site filter when approval queues need to be reviewed site by site.

Version availability

As of July 24, 2026, these features are available in:

  • Version 4.1.0 for Optimizely CMS 13 / .NET 10
  • Version 3.1.0 for the CMS 12 branch

Change Approval support is currently available only in the CMS 12 branch, because EPiServer.ChangeApproval does not yet have a CMS 13-compatible release.

Install or update

dotnet add package AdvancedTaskManager

Links

GitHub repository
NuGet package

Conclusion

This release makes Advanced Task Manager much more useful for teams handling larger approval volumes across pages, blocks, media, sites, and languages. The focus here is less about adding features for their own sake and more about making approval workflows faster, clearer, and easier to control in real Optimizely projects.

If you run into issues or want to suggest further improvements, the source code, changelog, and issue tracker are available on GitHub.

July 21, 2026 →

Friday, 10 July 2026

Optimizely DXP: Every Supported Culture, One Searchable Page

Quick one for anyone building multi-language sites on Optimizely DXP. I put together a reference tool listing all 806 supported cultures. More usefully, it shows which ones actually work with Azure Translator's 1-click auto-translate, and which get full NLP treatment (stemming, tokenization, decompounding) from Optimizely Graph.

If you've ever added a new market and then spent twenty minutes digging through docs to figure out whether editors will get auto-translate for that locale, or whether search will actually work well in that language, this is for that exact moment. Search or filter by culture name or code, grab the exact culture code you need for config, and see right away which capabilities you're getting.

806 cultures. 33 with Azure Translator support. Full Graph NLP coverage mapped out. All in one table.

Check it out here: Optimizely DXP – Supported Cultures / Languages

July 10, 2026 →