Programmatic Content Management
Outline
- Goal: Create and update CMS content via code for automation, integrations, and repeatable workflows.
-
Core APIs:
IContentLoader(read),IContentRepository(write),CreateWritableClone()(immutability rule). - Typical scenarios: Create pages, update blocks from external systems, create language variants, and control publish states.
- Operational note: Programmatic writes affect versioning, approvals, and (often) indexing—so treat bulk changes as release-grade operations.
Introduction
In modern CMS implementations, “content” rarely lives in one place. You may need to synchronize data from external systems, generate pages at scale, run scheduled updates, or implement controlled publishing workflows. Optimizely CMS 12 provides APIs that make these scenarios possible without relying on manual editor actions for every change.
This page walks through common scenario patterns for creating and updating content programmatically in a CMS 12 (PaaS) solution. The examples focus on repeatable, safe patterns that align with Optimizely’s versioning and security model.
1. Core concepts to keep straight
Most scenario-based implementations boil down to three rules: use the right interface for intent, clone before you modify, and choose the right
SaveAction based on your workflow.
-
IContentLoaderis for reading. Prefer it forGet<T>,GetItems, andGetChildren. -
IContentRepositoryis for writing. Use it forGetDefault<T>,Save,Delete, andMove. -
CreateWritableClone()is non-negotiable when updating existing items.
Expand: A quick mental model (read vs write)
- If your method should never change content, inject
IContentLoader. - If your method can change content, inject
IContentRepository(and still useIContentLoaderfor reads). - Updates are always: load → clone → modify → save.
2. Scenario 1: Creating a new page programmatically
Page creation typically happens in imports, synchronization jobs, or “generate pages from a dataset” flows. The stable approach is
GetDefault<T>(parent) → set properties → Save with the correct action.
Expand: Steps to create and publish a page
- Identify the parent
ContentReference. - Create the instance via
GetDefault<T>. - Set typed properties (preferred) or properties via
Propertycollection. - Save with
SaveActionthat matches the workflow (draft, request approval, publish).
using System;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAccess;
using EPiServer.Security;
// Assume StandardPage is a PageData type in your solution.
// public class StandardPage : PageData { public virtual string MainBody { get; set; } }
public class PageCreationService
{
private readonly IContentRepository _contentRepository;
public PageCreationService(IContentRepository contentRepository)
{
_contentRepository = contentRepository;
}
public ContentReference CreateAndPublishNewPage(string pageName, string mainBodyContent, ContentReference parentPageLink)
{
var newPage = _contentRepository.GetDefault<StandardPage>(parentPageLink);
newPage.PageName = pageName;
newPage.MainBody = mainBodyContent;
_contentRepository.Save(newPage, SaveAction.Publish, AccessLevel.Publish);
return newPage.ContentLink;
}
}
If you’re generating many pages, include logging and a rollback plan. In large batches, failures are inevitable—surprises shouldn’t be.
3. Scenario 2: Updating an existing block based on external data
Integration-driven updates are a classic “load → clone → modify → save” loop. The key discipline is to keep reads on IContentLoader,
and only switch to IContentRepository when you save.
using System;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAccess;
using EPiServer.Security;
// Assume ProductBlock is a BlockData type in your solution.
// public class ProductBlock : BlockData { public virtual decimal Price { get; set; } public virtual string ProductName { get; set; } }
public class BlockUpdateService
{
private readonly IContentRepository _contentRepository;
private readonly IContentLoader _contentLoader;
public BlockUpdateService(IContentRepository contentRepository, IContentLoader contentLoader)
{
_contentRepository = contentRepository;
_contentLoader = contentLoader;
}
public void UpdateProductBlockPrice(ContentReference blockLink, decimal newPrice)
{
var existing = _contentLoader.Get<ProductBlock>(blockLink);
var writable = existing.CreateWritableClone() as ProductBlock;
writable.Price = newPrice;
_contentRepository.Save(writable, SaveAction.Publish, AccessLevel.Publish);
}
}
Expand: Hardening an external-data update
- Validate the payload: treat external input as untrusted; apply business rules before saving.
- Be explicit about state: consider draft saves for changes that require review.
- Make updates idempotent: repeated runs should not create unintended changes.
- Log with identifiers: log content IDs and correlation IDs from the external system.
4. Scenario 3: Creating content in a specific language
Multilingual creation is the same pattern as page creation, with one difference: the language version is created by calling
GetDefault<T>(parent, culture).
using System.Globalization;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAccess;
using EPiServer.Security;
public class MultilingualContentService
{
private readonly IContentRepository _contentRepository;
public MultilingualContentService(IContentRepository contentRepository)
{
_contentRepository = contentRepository;
}
public ContentReference CreatePageInSpecificLanguage(ContentReference parent, CultureInfo language, string name, string body)
{
var newPage = _contentRepository.GetDefault<StandardPage>(parent, language);
newPage.PageName = name;
newPage.MainBody = body;
_contentRepository.Save(newPage, SaveAction.Publish, AccessLevel.Publish);
return newPage.ContentLink;
}
}
Expand: Language version gotchas
- Fallback isn’t creation: fallback loading finds content; it doesn’t create missing language versions.
- Keep naming conventions consistent: avoid “same page, different name” chaos across locales.
- Workflow alignment: localized content often needs approval gates per market/team.
5. Scenario 4: Managing publishing status (draft vs published vs scheduled)
Publishing control is expressed via SaveAction. The important part is to be deliberate: draft saves are not a “lesser publish”;
they are a workflow decision.
using System;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAccess;
using EPiServer.Security;
public class PublishingService
{
private readonly IContentRepository _contentRepository;
private readonly IContentLoader _contentLoader;
public PublishingService(IContentRepository contentRepository, IContentLoader contentLoader)
{
_contentRepository = contentRepository;
_contentLoader = contentLoader;
}
public void SaveAsDraft(ContentReference contentLink, Action<PageData> mutate)
{
var existing = _contentLoader.Get<PageData>(contentLink);
var writable = existing.CreateWritableClone() as PageData;
mutate(writable);
_contentRepository.Save(writable, SaveAction.Save, AccessLevel.Edit);
}
public void PublishNow(ContentReference contentLink, Action<PageData> mutate)
{
var existing = _contentLoader.Get<PageData>(contentLink);
var writable = existing.CreateWritableClone() as PageData;
mutate(writable);
_contentRepository.Save(writable, SaveAction.Publish, AccessLevel.Publish);
}
public void SchedulePublish(ContentReference contentLink, DateTime publishDate)
{
var existing = _contentLoader.Get<PageData>(contentLink);
var writable = existing.CreateWritableClone() as PageData;
writable.StartPublish = publishDate;
_contentRepository.Save(writable, SaveAction.CheckIn | SaveAction.Schedule, AccessLevel.Publish);
}
}
Expand: Choosing the right SaveAction
-
SaveAction.Savekeeps it as a draft (not live). -
SaveAction.RequestApprovalis for workflows with reviewers/approvers. -
SaveAction.Publishmakes the version live immediately. -
SaveAction.CheckIn | SaveAction.Schedulemoves content into delayed publish. -
Use caution: flags like
ForceCurrentVersioncan overwrite version history.
6. Best practices for scenario-based operations
- Wrap operations in guardrails: validate inputs, enforce business rules, and fail fast on invalid data.
-
Be explicit with permissions: choose
AccessLevelto match the action (edit vs publish). - Log like you’ll need it later: content IDs, external identifiers, version references, and the exact action taken.
- Plan for scale: bulk writes can cause indexing load and longer processing time; batch and schedule responsibly.
- Prefer deterministic behavior: avoid “works locally” drift by keeping versions aligned and processes repeatable.
Conclusion
Programmatic content creation and updates are core to automation, integration, and controlled delivery in Optimizely CMS 12. When you consistently apply
the patterns in this page—read with IContentLoader, modify with writable clones, and save with deliberate SaveAction choices—you
get predictable outcomes without undermining versioning, workflow, or security.
