Skip to main content

Outline

At a glance
  • Categories: A hierarchical classification system that editors can apply to content, enabling filtering and discovery.
  • Developer reality: In CMS 12, categories are treated as content items, so you typically load them via IContentLoader and assign them via writable clones + IContentRepository.
  • Site definitions: Multi-site configuration is modeled through SiteDefinition and accessed programmatically via ISiteDefinitionRepository.
  • Rule of thumb: Use SiteDefinition.Current inside a web request; use ISiteDefinitionRepository for listing/automation (jobs, tools, reporting).


Introduction

Building on the fundamentals of IContentRepository for content manipulation, this page dives into two commonly used areas of CMS 12 development: managing content classification through Categories, and handling multi-site setups through Site Definitions. Both topics become especially relevant in PaaS solutions where consistency, governance, and predictable behavior matter as much as “it works on my machine.”



Part 1. Managing categories in Optimizely CMS 12

Categories provide a structured way to classify content. Editors apply them to items to support filtering, discovery, and tailored experiences. In CMS 12, a practical mental model is: categories behave like content items from an API perspective, so you can retrieve and traverse them using the same patterns you already use for pages and blocks.



1. Understanding categories and their role

Categories are hierarchical. A category can contain sub-categories, allowing you to model taxonomy (topics, departments, product lines) in a way that both editors and developers can rely on. The admin UI is where editors manage them, but developers often need read access (for filtering logic) and write access (for automated assignment or migrations).

Expand: What you should constrain (taxonomy governance)
  • Keep depth reasonable: overly deep trees create UX pain in the editor picker.
  • Define naming rules: “Marketing” vs “Marketing Team” drift becomes costly later.
  • Decide ownership: who can create categories and who can only assign them.


2. Accessing categories programmatically

For read operations, inject IContentLoader. For writing/assigning categories to content, you’ll also need IContentRepository. Treat this like every other CMS write: load → clone → modify → save.

2.1 Inject loaders/repositories into your service

using EPiServer;
using EPiServer.Core;

public class CategoryService
{
    private readonly IContentLoader _contentLoader;
    private readonly IContentRepository _contentRepository;

    public CategoryService(IContentLoader contentLoader, IContentRepository contentRepository)
    {
        _contentLoader = contentLoader;
        _contentRepository = contentRepository;
    }
}

2.2 Loading a specific category by ContentReference

If you have a category reference (from an editor selection, a stored list, or a known ID), you can load it like other content types. Make sure you handle “not found” and “wrong type” scenarios cleanly.

using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;

var categoryLink = new ContentReference(123); // replace with real category ref/ID

try
{
    var category = _contentLoader.Get<Category>(categoryLink);
    // category.Name, category.Description, etc.
}
catch (TypeMismatchException)
{
    // The reference exists, but isn't a Category.
}
catch (ContentNotFoundException)
{
    // The reference doesn't exist.
}

2.3 Listing categories (traversing the hierarchy)

Categories are hierarchical, so listing children is a common way to traverse. For large or deep trees, consider lazy-loading (only retrieving children when needed) rather than dumping the entire taxonomy.

Expand: Example – list categories under a parent and recurse
using System.Collections.Generic;
using System.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;

ContentReference categoryRoot = ContentReference.GlobalBlockFolder; // example only

IEnumerable<Category> topLevel = _contentLoader.GetChildren<Category>(categoryRoot);

foreach (var c in topLevel)
{
    Console.WriteLine($"{c.Name} ({c.ContentLink.ID})");
    ListSubCategories(c.ContentLink, _contentLoader, "  ");
}

static void ListSubCategories(ContentReference parent, IContentLoader loader, string indent)
{
    var children = loader.GetChildren<Category>(parent);
    foreach (var child in children)
    {
        Console.WriteLine($"{indent}{child.Name} ({child.ContentLink.ID})");
        ListSubCategories(child.ContentLink, loader, indent + "  ");
    }
}
Reality check

The “root” reference for your categories depends on how your solution organizes taxonomy. Treat the root as a configurable concern rather than a constant.



3. Assigning categories to content

Content items commonly expose a CategoryList property (often surfaced as Category on PageData). Programmatically, you update this the same way you update any content property: create a writable clone, modify, then save.

using EPiServer.Core;
using EPiServer.DataAccess;
using EPiServer.Security;
using EPiServer.Web;
using EPiServer.DataAbstraction;

ContentReference contentToUpdate = ContentReference.StartPage;

var page = _contentLoader.Get<PageData>(contentToUpdate);
var writable = page.CreateWritableClone() as PageData;

// CategoryList stores category IDs (ints)
writable.Category.Add(123);
writable.Category.Add(456);

_contentRepository.Save(writable, SaveAction.Publish, AccessLevel.Publish);
Expand: Common mistakes when assigning categories
  • Forgetting to clone: updates must be applied to a writable clone.
  • Using ContentReference directly: CategoryList generally stores integer IDs.
  • Publishing unintentionally: use SaveAction.Save when you need draft + approval workflows.


4. Best practices for categories

  • Design the hierarchy deliberately: taxonomy is a product decision, not a “just add tags” feature.
  • Apply consistently: inconsistent category usage makes filters and personalization unreliable.
  • Prefer search indexes for category filtering at scale: large datasets often benefit from indexed search patterns.
  • Treat category changes as migrations: renames and restructures can have downstream effects in code and UX.


Part 2. Working with site definitions using ISiteDefinitionRepository

Optimizely CMS can host multiple sites in a single installation. Each site is represented by a SiteDefinition which contains the site’s start page, host mappings, and configuration used to resolve “which site is this request for?”



5. Understanding SiteDefinition

A SiteDefinition captures key site-level settings: name, start page reference, hosts, and site URL. These are typically managed in the CMS admin UI, but developers often need to read them for routing logic, reporting, synchronization, or scheduled jobs.

Expand: Commonly used SiteDefinition properties
  • Id – unique identifier for the site.
  • Name – display name of the site.
  • StartPageContentReference to the site’s root.
  • Hosts – host mappings (HostDefinition) including language routing.
  • SiteUrl – primary URL for the site.


6. Obtaining ISiteDefinitionRepository via DI

Like other Optimizely services, ISiteDefinitionRepository should be injected. This is the reliable approach for scheduled jobs and services that need to reason about sites outside of an HTTP request.

using EPiServer.Web;

public class SiteDefinitionService
{
    private readonly ISiteDefinitionRepository _siteDefinitionRepository;

    public SiteDefinitionService(ISiteDefinitionRepository siteDefinitionRepository)
    {
        _siteDefinitionRepository = siteDefinitionRepository;
    }
}


7. Retrieving site definitions

The repository supports listing all configured sites and retrieving a specific site by ID or name. This is useful for multi-site tooling, reporting, and cross-site synchronization jobs.

7.1 Listing all site definitions

using System.Linq;
using EPiServer.Web;

var allSites = _siteDefinitionRepository.List();

foreach (var site in allSites)
{
    Console.WriteLine($"Site: {site.Name} (ID: {site.Id})");
    Console.WriteLine($"  Start page: {site.StartPage.ID}");
    Console.WriteLine($"  Site URL: {site.SiteUrl}");
    foreach (var host in site.Hosts)
    {
        Console.WriteLine($"  Host: {host.Name} (Language: {host.Language})");
    }
}

7.2 Get a specific site by ID or by name

using EPiServer.Web;

// By ID
var siteById = _siteDefinitionRepository.Get(1);

// By name
var siteByName = _siteDefinitionRepository.Get("My Main Site");

7.3 Access the current site definition (SiteDefinition.Current)

In a web request context, SiteDefinition.Current is convenient and avoids repository calls. Outside a request (jobs/background work), prefer the repository because “current site” may not exist.

using EPiServer.Web;

var currentSite = SiteDefinition.Current;

if (currentSite != null)
{
    Console.WriteLine(currentSite.Name);
    Console.WriteLine(currentSite.StartPage.ID);
    Console.WriteLine(currentSite.SiteUrl);
}


8. Best practices for site definitions

  • Request context: use SiteDefinition.Current for the current request’s site.
  • Automation/tools: use ISiteDefinitionRepository for listing and cross-site processing.
  • Host mappings matter: language and hostname routing issues often trace back to host configuration.
  • Be explicit in jobs: loop through List() and act per-site rather than assuming a single global context.

Conclusion

Categories and site definitions are two APIs you’ll touch frequently in real CMS 12 solutions. Categories help keep content discoverable and filterable, while site definitions are foundational for multi-site architectures. When you treat categories as part of a governed taxonomy and treat site definitions as operational configuration (especially in scheduled jobs), you end up with systems that scale without becoming fragile.