Take the community feedback survey now.

Tomek Juranek
Jul 22, 2025
  575
(3 votes)

Branch Templates in Optimizely CMS

Optimizely CMS natively doesn't support branch templates, a concept known from different content management systems. Branch templates are useful if we want to help content authors to automate repetetive tasks with multiple content items, for example creation of a new product pages with subpages, all with predefined layout containing various blocks. This issue can be solved by copying and pasting exisitng page and adjusting the content on the copy, but it's more like a workaround which solves the problem only partially. For the blocks initial values in Optimizely CMS we can use SetDefaultValues method but managing it from code for larger structures can be painful, also it doesn't solve the problem of automated subpages creation. We can solve both issues by adding branch template functionality to the CMS.

We can achieve it by adding simple code, first let's add a new item template, we will only use it as a folder, so we don't need any fields:

[ContentType(DisplayName = "White Label Template Folder",
        GUID = "2acce58b-41cc-4edc-a361-a1baa86b51bc",
        Description = "A folder which allows to structure new branch templates.",
        GroupName = TabNames.BranchTemplate, Order = 40)]
public class TemplateFolderPage : PageData
{
}

We can also configure the folder icon:

[UIDescriptorRegistration]
public class TemplateFolderPageUIDescriptor : UIDescriptor<TemplateFolderPage>
{
    public TemplateFolderPageUIDescriptor()
            : base(ContentTypeCssClassNames.Folder)
    {
    }
}

To implement the core functionality we can use the code below. In a nutshell, inside the CreatedContent event, we first do some pre-checks:
- Is it a "new item" event? (we don't want to execute the code for example on copy/paste opetation). 
- Do we have a folder with our branch templates under the root?
- Do we have a page item with the currently created type inside the root branch templates folder? 
If all checks passed, we create a deep copy of the item from the branch folder and use it to replace the original page. We make sure that page name and URLSegment are unique:

[InitializableModule]
[ModuleDependency(typeof(InitializationModule))]
public class BranchTemplateInitialization : IInitializableModule
{
    private IContentRepository _contentRepository;
    private IContentLoader _contentLoader;
    private UrlSegmentOptions _urlSegmentOptions;
    private IUniqueIdentityCreator _uniqueIdentityCreator;

    public void Initialize(InitializationEngine context)
    {
        var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
        _contentRepository ??= ServiceLocator.Current.GetInstance<IContentRepository>();
        _contentLoader ??= ServiceLocator.Current.GetInstance<IContentLoader>();
        _urlSegmentOptions ??= ServiceLocator.Current.GetInstance<UrlSegmentOptions>();
        _uniqueIdentityCreator ??= ServiceLocator.Current.GetInstance<IUniqueIdentityCreator>();

        contentEvents.CreatedContent += ContentEvents_CreatedContent;
    }

    public void Uninitialize(InitializationEngine context)
    {
        var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
        contentEvents.CreatedContent -= ContentEvents_CreatedContent;
    }

    private void ContentEvents_CreatedContent(object sender, ContentEventArgs e)
    {
        var saveArgs = e as SaveContentEventArgs;
        if (e.Content == null || (saveArgs != null && saveArgs.MaskedAction == SaveAction.CheckOut))
        {
            // skip other actions like copying
            return;
        }

        var templateFolder = _contentLoader.GetChildren<TemplateFolderPage>(ContentReference.RootPage).FirstOrDefault();
        if (templateFolder == null) 
        { 
            return; 
        }

        // this is simplified code which only take 1st branch template item with the given type. 
        var myTemplateItem = _contentLoader.GetChildren<IContent>(templateFolder.ContentLink)?.FirstOrDefault(x => x.ContentTypeID == e.Content.ContentTypeID);
        if (myTemplateItem == null) 
        {
            return; 
        }

        var name = e.Content.Name;
        var oldItemLink = e.ContentLink;
        var parentLink = e.Content.ParentLink;
        var newItemLink = _contentRepository.Copy(myTemplateItem.ContentLink, parentLink, AccessLevel.NoAccess, AccessLevel.NoAccess, false);
        var newItem = _contentRepository.Get<PageData>(newItemLink);
        _contentRepository.Delete(oldItemLink, true);

        // rename page name and url segment to the one entered by author
        var newPage = newItem.CreateWritableClone();
        // we need to rename to new name and before forcing uniqueness
        newPage.Name = name;
        newPage.URLSegment = name;
        newPage.Name = _uniqueIdentityCreator.CreateName(newPage, name);
        newPage.URLSegment = _uniqueIdentityCreator.CreateURLSegment(newPage, _urlSegmentOptions);
        _contentRepository.Save(newPage, SaveAction.Default, AccessLevel.NoAccess);

        // update content link to point to new item
        e.ContentLink = newItemLink;
        e.Content = newItem;
    }
}

To make it work, we need to create the folder of type TemplateFolderPage under the root (I called it "Products Wizard"), then inside that folder we create a new page of selected type and predefined layout (I called it "Simple Product Template" and used "Product Page" type, but this is just an example). I also added a sub page called "Gallery" with it's own layout. This structure will serve as a template for all new product pages:

With the code in place, we can now create w new "Product Page" item using the standard "New Page" dialog. It should create a new product page under the given name and url segment, but with the predefined layout and the gallery subpage inside.

Jul 22, 2025

Comments

Soren S
Soren S Jul 23, 2025 06:11 AM

Thanks 

Praful Jangid
Praful Jangid Jul 30, 2025 11:42 AM

I personally would like to try it out. It looks good and might reduce some CA over heads.

Thanks for sharing it Tomek Juranek.

Scott Reed
Scott Reed Jul 30, 2025 02:24 PM

CMS 13 coming Q1 next year with the new experience builder is setup to support reusable sections/pages and such

Ravindra S. Rathore
Ravindra S. Rathore Jul 30, 2025 06:02 PM

A couple of days back, someone working on some other cms platform was asking about this. Thanks for sharing 

Tomek Juranek
Tomek Juranek Jul 31, 2025 06:22 AM

The other platform name starts with "s", ends with "e" and has no captical "c" in the middle? just asking ;) 

Please login to comment.
Latest blogs
How to Add Multiple Authentication Providers to an Optimizely CMS 12 Site (Entra ID, Google, Facebook, and Local Identity)

Modern websites often need to let users sign in with their corporate account (Entra ID), their social identity (Google, Facebook), or a simple...

Francisco Quintanilla | Oct 22, 2025 |

Connecting the Dots Between Research and Specification to Implementation using NotebookLM

Overview As part of my day to day role as a solution architect I overlap with many clients, partners, solutions and technologies. I am often...

Scott Reed | Oct 22, 2025

MimeKit Vulnerability and EPiServer.CMS.Core Dependency Update

Hi everyone, We want to inform you about a critical security vulnerability affecting older versions of the EPiServer.CMS.Core  package due to its...

Bien Nguyen | Oct 21, 2025

Speeding Up Local Development with a Fake OpenID Authentication Handler

When working with OpenID authentication, local development often grinds to a halt waiting for identity servers, clients, and users to be configured...

Eric Herlitz | Oct 20, 2025 |

How Optimizely MCP Learns Your CMS (and Remembers It)

In Part 1, I introduced the “discovery-first” idea—an MCP that can plug into any SaaS CMS and learn how it’s structured on its own. This post gets...

Johnny Mullaney | Oct 20, 2025 |

Building a Custom Payment in Optimizely Commerce 14 (with a simple “Account” method)

This post outlines a simplified method for integrating Account payments into Commerce 14, detailing a minimal working path without extensive...

Francisco Quintanilla | Oct 17, 2025 |