Exam Area: Content Area 4 – Framework Components (15%)
Source: https://academy.optimizely.com/student/path/3128969/activity/4970351 | /4970354
Published: Mar 5, 2026
- Purpose:
IContentProviderexposes external data as if it were native CMS content — no duplication.- Architecture: External systems remain the source of truth; Optimizely handles presentation, routing, and editorial visibility.
- Key classes: Extend
EPiServer.Core.ContentProvider, implementLoadContent+LoadChildrenReferencesAndTypes.- IPartialRouter: Maps URLs to external content items for SEO-friendly URLs.
- Limitation: Content from custom providers is NOT automatically indexed by native CMS search.
Content Providers allow external content to be integrated into the CMS page tree as if it were native CMS content. Instead of duplicating data into the CMS database, the provider virtualizes external data and loads it on demand.
Use cases: product catalog, documents from SharePoint, external news feeds, third-party editorial services.
using EPiServer.Core;
using EPiServer.DataAbstraction;
[ContentProvider]
public class ExternalArticleContentProvider : ContentProvider
{
private readonly ExternalApiService _externalApiService;
private readonly IContentTypeRepository _contentTypeRepository;
public ExternalArticleContentProvider(
ExternalApiService externalApiService,
IContentTypeRepository contentTypeRepository)
{
_externalApiService = externalApiService;
_contentTypeRepository = contentTypeRepository;
}
// Map URL → ContentReference
public override ContentReference GetContentLink(Uri url)
{
var segments = url.Segments;
if (segments.Length >= 2 &&
segments[1].Trim('/').Equals("external-articles", StringComparison.OrdinalIgnoreCase))
{
var externalId = segments[2].Trim('/').Split('/')[0];
return new ContentReference(0, externalId, ProviderKey);
}
return ContentReference.EmptyReference;
}
// Load content on demand
protected override IContent LoadContent(ContentReference contentLink, LanguageSelector selector)
{
if (!string.IsNullOrEmpty(contentLink.ProviderValue))
{
var externalId = contentLink.ProviderValue;
var externalData = _externalApiService.GetArticleById(externalId);
if (externalData != null)
{
var page = CreateContent<ExternalArticlePage>(contentLink);
page.ExternalTitle = externalData.Title;
page.ExternalBody = new XhtmlString(externalData.BodyHtml);
page.ExternalId = externalData.Id;
page.Name = externalData.Title;
page.ContentGuid = GetContentGuid(externalId);
page.Status = VersionStatus.Published;
page.StartPublish = DateTime.MinValue;
page.StopPublish = DateTime.MaxValue;
return page;
}
}
return null;
}
// Return children for the page tree
protected override IList<GetChildrenReferenceResult> LoadChildrenReferencesAndTypes(
ContentReference contentLink, string languageID, ref bool languageSpecific)
{
var children = new List<GetChildrenReferenceResult>();
if (contentLink == EntryPoint)
{
foreach (var article in _externalApiService.GetAllArticles())
{
var childRef = new ContentReference(0, article.Id, ProviderKey);
children.Add(new GetChildrenReferenceResult
{
ContentLink = childRef,
IsLeafNode = true,
ModelType = typeof(ExternalArticlePage)
});
}
}
return children;
}
}
[ContentType(
DisplayName = "External Article Page",
GUID = "A2FF8C8B-1E5A-4B6A-B6A0-142B28B3EF4A",
AvailableInEditMode = false // editors cannot create these manually
)]
public class ExternalArticlePage : PageData
{
[CultureSpecific]
[Display(Name = "External Title", Order = 10)]
public virtual string ExternalTitle { get; set; }
[CultureSpecific]
[Display(Name = "External Body", Order = 20)]
public virtual XhtmlString ExternalBody { get; set; }
public virtual string ExternalId { get; set; }
public virtual string ExternalUrl { get; set; }
}
public void ConfigureServices(IServiceCollection services)
{
services.AddCms();
services.AddSingleton<ExternalApiService>();
services.AddContentProvider<ExternalArticleContentProvider>();
}
public class ExternalArticlePartialRouter : IPartialRouter<PageData, ExternalArticlePage>
{
private readonly ExternalApiService _externalApiService;
public ExternalArticlePartialRouter(ExternalApiService externalApiService)
{
_externalApiService = externalApiService;
}
public PartialRouteData Get(PageData content, SegmentContext segmentContext)
{
var nextSegment = segmentContext.RemainingPath;
if (string.IsNullOrEmpty(nextSegment)) return null;
var parts = nextSegment.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var externalId = parts[0];
var externalArticle = _externalApiService.GetArticleById(externalId);
if (externalArticle != null)
{
var contentLink = new ContentReference(0, externalArticle.Id, "ExternalContentProviderKey");
segmentContext.RemainingPath = (parts.Length > 1) ? string.Join("/", parts.Skip(1)) : string.Empty;
return new PartialRouteData
{
ContentLink = contentLink,
RouteValues = new Dictionary<string, object> { { "externalId", externalId } }
};
}
return null;
}
}
// Registration
services.AddTransient<ExternalArticlePartialRouter>();
services.AddSingleton<IRegisterPartialRoutes>(ctx => {
var router = ctx.GetService<ExternalArticlePartialRouter>();
return new RegisterPartialRoutes<ContainerPage>(router);
});
Root
└── My Site
├── Home
└── Articles (Entry Point) ← provider attaches here
├── Article A (external, from API)
├── Article B (external, from API)
└── Article C (external, from API)
| Concern | Practice |
|---|---|
| Performance | Cache external API calls — use IMemoryCache or IDistributedCache at the provider level |
| GUID stability | Generate a deterministic ContentGuid from the external ID — required for links and indexing |
| Error handling | Graceful fallback in LoadContent — external APIs can fail |
| Search indexing | External provider content is NOT automatically indexed — must be handled manually |
| Security | External HTML → sanitize before storing in XhtmlString |
| Write-back | If needed, override Save() — providers are read-only by default |
LoadContent() — retrieves and maps external data to IContent)AvailableInEditMode = false on a content type mean? (Editors cannot manually create these — only the provider creates them)IPartialRouter used for? (Maps URLs to external content items — SEO-friendly URLs)