🧩 Framework Components
Content Provider
📖 Docs

Content Providers - Optimizely CMS 12

Exam Area: Content Area 4 – Framework Components (15%)
Source: https://academy.optimizely.com/student/path/3128969/activity/4970351 | /4970354
Published: Mar 5, 2026


Overview (Academy Key Points)

  • Purpose: IContentProvider exposes 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, implement LoadContent + 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.

1. What Are Content Providers? (Academy – 4970354)

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.


2. Creating a Custom Content Provider (Academy – 4970354)

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;
    }
}

3. Content Type for External Data (Academy – 4970354)

[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; }
}

4. Registering a Content Provider

public void ConfigureServices(IServiceCollection services)
{
    services.AddCms();
    services.AddSingleton<ExternalApiService>();
    services.AddContentProvider<ExternalArticleContentProvider>();
}

5. IPartialRouter – URL Mapping (Academy – 4970351)

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);
});

6. Content Provider Page Tree Structure

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)

7. Best Practices (Academy – 4970354)

ConcernPractice
PerformanceCache external API calls — use IMemoryCache or IDistributedCache at the provider level
GUID stabilityGenerate a deterministic ContentGuid from the external ID — required for links and indexing
Error handlingGraceful fallback in LoadContent — external APIs can fail
Search indexingExternal provider content is NOT automatically indexed — must be handled manually
SecurityExternal HTML → sanitize before storing in XhtmlString
Write-backIf needed, override Save() — providers are read-only by default

Review Questions

  1. What are Content Providers used for? (Integrating external content into the CMS page tree — virtualized, no data duplication)
  2. Which method is the critical implementation point? (LoadContent() — retrieves and maps external data to IContent)
  3. What does AvailableInEditMode = false on a content type mean? (Editors cannot manually create these — only the provider creates them)
  4. What is IPartialRouter used for? (Maps URLs to external content items — SEO-friendly URLs)
  5. Why is a stable/deterministic ContentGuid needed? (Required for search indexing, permanent links, and caching consistency)
  6. Is content from a custom provider automatically indexed? (No — indexing must be implemented manually)