In this blog, I’ll walk you through how to create a custom Site Settings tab in the Assets pane of Optimizely CMS. This tab will allow you to manage site-specific configurations such as data sources, global settings, and even navigation menus in a centralized, scalable way.
This solution was developed to meet a client requirement for making site configurations easier to manage as the site grows in size and complexity.
The approach is broken down into four key steps, each targeting a critical part of the setup:
Step 1: Define Base Classes for Settings Blocks and Folders
The first step is to lay the foundation for reusable, structured site settings by defining base classes and setting up supporting services.
We’ll create:
SettingsBase
A base class for all site setting blocks.
Extend from StandardContentBase
(or BlockData
, depending on your architecture).
This will be the shared base for content types like LayoutSettings
, SeoSettings
, etc.
SettingsFolder
A content folder type used to organize settings by site.
Each website will have its own folder under the Site Settings tab, containing that site’s configuration blocks.
To define this, extend from ContentFolder
and assign a unique GUID and name.
🧱 Per-site Settings Blocks
Each site can define specific settings blocks — for example, a LayoutSettings
block that inherits from SettingsBase
.
These blocks can contain fields such as logos, footers, menu configurations, or any other global content required.
By standardizing settings structures up front, we make them easier to manage, validate, and fetch consistently across the solution.
public class SettingsService : ISettingsService
{
private readonly IContentRepository _contentRepository;
private readonly IContentVersionRepository _contentVersionRepository;
private readonly ContentRootService _contentRootService;
private readonly IContentTypeRepository _contentTypeRepository;
private readonly EPiServer.Logging.ILogger _log = LogManager.GetLogger();
private readonly IContextModeResolver _contextModeResolver;
private readonly ISynchronizedObjectInstanceCache _objectCache;
private readonly IContentCacheKeyCreator _contentCacheKeyCreator;
private const string StartPageType = "StartPage";
private const string SettingsFolderPropName = "SettingsFolder";
private const string SiteSettingsCachePrefix = "SiteSettingsCache";
public SettingsService(
IContentRepository contentRepository,
IContentVersionRepository contentVersionRepository,
ContentRootService contentRootService,
IContentTypeRepository contentTypeRepository,
IContextModeResolver contextModeResolver,
ISynchronizedObjectInstanceCache objectCache,
IContentCacheKeyCreator contentCacheKeyCreator)
{
_contentRepository = contentRepository;
_contentVersionRepository = contentVersionRepository;
_contentRootService = contentRootService;
_contentTypeRepository = contentTypeRepository;
_contextModeResolver = contextModeResolver;
_objectCache = objectCache;
_contentCacheKeyCreator = contentCacheKeyCreator;
}
public ContentReference? GlobalSettingsRoot { get; set; }
public string GetSiteSettingsCacheKey(ContentReference startPageRef, Type settingsType, bool isDraft = false, string contentLanguage = "")
{
var draftSegment = isDraft ? "-common-draft" : string.Empty;
return $"{SiteSettingsCachePrefix}-{startPageRef.ID}-{draftSegment}-{contentLanguage}-{settingsType.Name}";
}
public T? GetSiteSettings<T>(ContentReference startPageReference, string language = "") where T : SettingsBase
{
try
{
var contentLanguage = string.IsNullOrEmpty(language) ? ContentLanguage.PreferredCulture.Name : language;
var isDraft = _contextModeResolver.CurrentMode == ContextMode.Edit;
var cacheKey = GetSiteSettingsCacheKey(startPageReference, typeof(T), isDraft, contentLanguage);
var settings = _objectCache.Get(cacheKey) as T;
if (settings != null)
return settings;
settings = LoadSettings<T>(startPageReference, isDraft, contentLanguage);
if (settings != null)
{
var evictionPolicy = isDraft ?
new CacheEvictionPolicy(new string[]
{
_contentCacheKeyCreator.CreateVersionCommonCacheKey(startPageReference),
_contentCacheKeyCreator.CreateVersionCommonCacheKey(settings.ContentLink)
})
: new CacheEvictionPolicy(new string[]
{
_contentCacheKeyCreator.CreateCommonCacheKey(startPageReference),
_contentCacheKeyCreator.CreateCommonCacheKey(settings.ContentLink)
});
_objectCache.Insert(cacheKey, settings, evictionPolicy);
}
return settings;
}
catch (ArgumentNullException argumentNullException)
{
_log.Error($"SettingsService : GetSiteSettings argumentNullException", exception: argumentNullException);
}
catch (NullReferenceException nullReferenceException)
{
_log.Error($"SettingsService : GetSiteSetting nullReferenceException", exception: nullReferenceException);
}
return default;
}
public T GetSiteSettings<T>() where T : SettingsBase
{
return this.GetSiteSettings<T>(
SiteDefinition.Current.StartPage,
CultureInfo.CurrentCulture.ToString());
}
public void InitializeSettings()
{
try
{
RegisterContentRoots();
}
catch (NotSupportedException notSupportedException)
{
_log.Error($"SettingsService : InitializeSettings ", exception: notSupportedException);
}
}
public T? LoadSettings<T>(ContentReference startPageRef, bool isDraft, string language) where T : SettingsBase
{
if (startPageRef != null && startPageRef.ID > 0)
{
try
{
var startPage = _contentRepository.Get<IContent>(startPageRef, new CultureInfo(language)) ??
_contentRepository.Get<IContent>(startPageRef);
if (startPage != null)
{
var settingsFolderRef = startPage.Property[SettingsFolderPropName].Value as ContentReference;
if (settingsFolderRef != null)
{
var settings = _contentRepository.GetChildren<T>(settingsFolderRef).FirstOrDefault();
if (settings != null)
{
if (isDraft)
{
var draftContentLink = _contentVersionRepository.LoadCommonDraft(settings.ContentLink, language);
if (draftContentLink != null)
{
var settingsDraft = _contentRepository.Get<T>(draftContentLink.ContentLink);
return settingsDraft;
}
}
if (settings.ExistingLanguages.Any(t => string.Equals(t.Name, language, StringComparison.InvariantCultureIgnoreCase)))
{
return _contentRepository.Get<T>(settings.ContentLink, new CultureInfo(language));
}
return settings;
}
}
}
}
catch (Exception ex)
{
_log.Error($"SettingsService : LoadSettings ", exception: ex);
}
}
return null;
}
private void RegisterContentRoots()
{
var registeredRoots = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions());
var settingsRootRegistered = registeredRoots.Any(x => x.ContentGuid == SettingsFolder.SettingsRootGuid && x.Name.Equals(SettingsFolder.SettingsRootName));
if (!settingsRootRegistered)
{
_contentRootService.Register<SettingsFolder>(SettingsFolder.SettingsRootName, SettingsFolder.SettingsRootGuid, ContentReference.RootPage);
}
var root = _contentRepository.GetItems(_contentRootService.List(), new LoaderOptions())
.FirstOrDefault(x => x.ContentGuid == SettingsFolder.SettingsRootGuid);
if (root == null)
return;
GlobalSettingsRoot = root.ContentLink;
}
/// <summary>
/// Resolve the start page from a page reference (the highest order ancestor which is of type Start Page)
/// </summary>
/// <param name="pageReference"></param>
/// <returns></returns>
public ContentReference? ResolveStartPageByReference(ContentReference pageReference)
{
var startPageType = _contentTypeRepository.Load(StartPageType);
if (_contentRepository.TryGet<PageData>(pageReference, out var page))
{
if (page != null)
{
if (Equals(page.ParentLink, ContentReference.RootPage) && page.ContentTypeID == startPageType.ID)
return pageReference;
var ancestors = _contentRepository.GetAncestors(pageReference);
if (ancestors.Count() < 2)
return null;
var startPage = ancestors.ElementAt(ancestors.Count() - 2);
if (startPage.ContentTypeID == startPageType.ID)
return startPage.ContentLink;
}
}
return null;
}
/// <summary>
/// Resolve the start page from a page pageGuid (the highest order ancestor which is of type Start Page)
/// </summary>
/// <param name="pageGuid"></param>
/// <returns></returns>
public ContentReference? ResolveStartPageByGuid(Guid pageGuid)
{
if (_contentRepository.TryGet<PageData>(pageGuid, out var page))
{
return ResolveStartPageByReference(page.ContentLink);
}
return null;
}
}
[ContentType(DisplayName = "Site Settings", GUID = "c41959cf-efc1-4e5f-b79d-78d3b8e5af13")]
public abstract class SettingsBase : StandardContentBase
{
}
[ContentType(GUID = "c709627f-ca9f-4c77-b0fb-8563287ebd93")]
[AvailableContentTypes(Include = new[] { typeof(SettingsBase), typeof(SettingsFolder) })]
public class SettingsFolder : ContentFolder
{
public const string SettingsRootName = "SettingsRoot";
public static Guid SettingsRootGuid = new Guid("79611ee5-7ddd-4ac8-b00e-5e8e8d2a57ee");
private Injected<LocalizationService> _localizationService;
private static Injected<ContentRootService> _rootService;
public static ContentReference SettingsRoot => GetSettingsRoot();
public override string Name
{
get
{
if (ContentLink.CompareToIgnoreWorkID(SettingsRoot))
{
var localizedName = _localizationService.Service.GetString("/contentrepositories/globalsettings/Name");
if (!string.IsNullOrWhiteSpace(localizedName))
return localizedName;
}
return base.Name;
}
set => base.Name = value;
}
private static ContentReference GetSettingsRoot() => _rootService.Service.Get(SettingsRootName);
}
Step 2: Create a Site Settings Tab in the Assets Pane
To make the Site Settings visible and manageable within the Optimizely CMS Assets pane, we need to complete two essential steps:
1. Extend ContentRepositoryDescriptorBase
This class defines the behavior and structure of a custom content repository in the CMS. By extending it, we specify:
The content types allowed inside the Site Settings tab (e.g., settings blocks, folders)
The root location for storing these settings
The name and behavior of the tab within the editor interface
2. Extend ComponentDefinitionBase
This is the second half of the equation. It registers the UI component that makes the tab appear in the Assets pane. Specifically, it:
Defines the label and sort order of the tab
Points to the
repositoryKey
defined in theContentRepositoryDescriptor
Specifies the area in the UI where the tab will be rendered (in this case, the Assets pane)
[ServiceConfiguration(typeof(IContentRepositoryDescriptor))]
public class GlobalSettingsRepositoryDescriptor : ContentRepositoryDescriptorBase
{
public static string RepositoryKey => "globalsettings";
public override IEnumerable<Type> ContainedTypes => new[] {
typeof(SettingsBase),
typeof(SettingsFolder)
};
public override IEnumerable<Type> CreatableTypes => new[] {
typeof(SettingsBase),
typeof(SettingsFolder)
};
public override string CustomNavigationWidget => "epi-cms/component/ContentNavigationTree";
public override string CustomSelectTitle => LocalizationService.Current.GetString("/contentrepositories/globalsettings/customselecttitle");
public override string Key => RepositoryKey;
public override IEnumerable<Type> MainNavigationTypes => new[]
{
typeof(SettingsBase),
typeof(SettingsFolder)
};
public override IEnumerable<string> MainViews => new string[1] { HomeView.ViewName };
public override string Name => "Site Settings";
public override IEnumerable<ContentReference> Roots => Settings.Service.GlobalSettingsRoot is null ? Array.Empty<ContentReference>() : new[] { Settings.Service.GlobalSettingsRoot };
// public override string SearchArea => GlobalSettingsSearchProvider.SearchArea;
public override int SortOrder => 5000;
//
private Injected<ISettingsService> Settings { get; set; }
}
[Component]
public class GlobalSettingsComponent : ComponentDefinitionBase
{
public GlobalSettingsComponent()
: base("epi-cms/component/MainNavigationComponent")
{
LanguagePath = "/episerver/cms/components/globalsettings";
base.Title = "Site settings";
SortOrder = 5000;
PlugInAreas = new[] { PlugInArea.AssetsDefaultGroup };
base.Settings.Add(new Setting("repositoryKey", value: GlobalSettingsRepositoryDescriptor.RepositoryKey));
}
}
Once these two classes are in place, you’ll see a new “Site Settings” tab alongside your existing media and block libraries in the CMS — giving editors a centralized place to manage configuration content.
Step 3: Update the Start Page to Reference Site Settings
On the Start Page item:
Add a new field of type Reference.
Name it something like “Site Layout”.
Set the Data Source or allowed target to point to your Site Settings item (or the area where the layout definitions are stored).
public class StartPage : SitePageData
{
[Display(Name = "Settings Folder",
Description = "You can add the custom settings (layout, header, footer) for each start page.",
GroupName = SystemTabNames.Settings, Order = 10)]
[AllowedTypes(typeof(SettingsFolder))]
public virtual ContentReference SettingsFolder { get; set; }
Step 4: Use the SiteSetting Created
Inject the ISettingsService
into your module (e.g., a controller or service), and use it to retrieve the relevant site-level configuration — such as LayoutSettings
, SearchSettings
, or any other settings class.
This allows the module to dynamically adapt its behavior based on the current site’s configuration, improving reusability and centralizing control.
Github Repository Url
https://github.com/haryaniparshant/OptimizelyBlogs
Absolutely fantastic guide! 🚀 This is the kind of practical, well-structured walkthrough that saves teams hours of trial and error. I especially appreciate how you’ve broken down each step — from the base classes to the CMS UI integration — and highlighted why this approach matters as sites scale. The focus on maintainability and reusability is spot on. Thanks for sharing such a clear and actionable solution for managing site-wide configurations in Optimizely CMS! 👏
Thanks Abdul Ghaffar Ansari