Skip to content

Custom Content Rendering

⏱ 20 minutes intermediate

Every content type needs a visual representation. By default, CMS routes page requests to a controller and view based on the content type name. When you need custom logic — loading related content, transforming data, or selecting different layouts — you build custom controllers and views.

  1. Create a page controller for a content type
  2. Build a Razor view that renders properties
  3. Add partial rendering for block types
  4. Register a display channel for mobile rendering

A page controller handles HTTP requests for a specific page type. Inherit from PageController<T> where T is your page type.

Page controller
csharp
using Microsoft.AspNetCore.Mvc;
using Optimizely.Cms.Core.Web;

namespace MySite.Controllers;

public class ArticlePageController : PageController<ArticlePage>
{
    public IActionResult Index(ArticlePage currentPage)
    {
        var model = new ArticleViewModel
        {
            Title = currentPage.Title,
            Body = currentPage.MainBody,
            Author = currentPage.AuthorName,
            Published = currentPage.StartPublish,
            ReadTime = EstimateReadTime(currentPage.MainBody)
        };

        return View(model);
    }

    private static int EstimateReadTime(XhtmlString? body)
    {
        if (body == null) return 0;
        var wordCount = body.ToString()?.Split(' ').Length ?? 0;
        return Math.Max(1, wordCount / 200);
    }
}

The CMS routing engine automatically maps requests for ArticlePage content to this controller. The currentPage parameter is populated by the framework.

Place the view at Views/ArticlePage/Index.cshtml to match the controller name convention.

Razor view for a page type
html
@model MySite.Models.ArticleViewModel

<article class="article">
    <header>
        <h1>@Model.Title</h1>
        <div class="meta">
            <span>By @Model.Author</span>
            <span>@Model.Published?.ToString("MMMM d, yyyy")</span>
            <span>@Model.ReadTime min read</span>
        </div>
    </header>

    <div class="article-body">
        @Html.Raw(Model.Body)
    </div>
</article>

Blocks render inside ContentArea properties. Create a partial controller that returns a partial view.

Block partial controller
csharp
using Microsoft.AspNetCore.Mvc;
using Optimizely.Cms.Core.Web;

namespace MySite.Controllers;

public class TestimonialBlockController
    : BlockController<TestimonialBlock>
{
    public override IActionResult Index(
        TestimonialBlock currentBlock)
    {
        return PartialView(currentBlock);
    }
}

Place the partial view at Views/TestimonialBlock/Index.cshtml.

Block partial view
html
@model MySite.Models.TestimonialBlock

<blockquote class="testimonial">
    <p>"@Model.Quote"</p>
    <footer>
        <strong>@Model.AuthorName</strong>
        <span>@Model.AuthorTitle</span>
    </footer>
</blockquote>

Display channels let you serve different views for different device types — for example, a simplified layout for mobile.

Mobile display channel
csharp
using Optimizely.Cms.Core.Web;
using Optimizely.Cms.Framework.Web;

namespace MySite.Channels;

public class MobileChannel : DisplayChannel
{
    public override string ChannelName => "Mobile";

    public override string ResolutionId =>
        typeof(MobileChannel).FullName!;

    public override bool IsActive(
        HttpContext context)
    {
        return context.Request.Headers["User-Agent"]
            .ToString()
            .Contains("Mobile", StringComparison.OrdinalIgnoreCase);
    }
}

When a mobile user agent is detected, the CMS looks for a view at Views/ArticlePage/Index.Mobile.cshtml. If it exists, that view is rendered instead of the default.

When a content type has multiple possible renderings, register template descriptors so editors can choose.

Template descriptor
csharp
using Optimizely.Cms.Core.Web;

namespace MySite.Controllers;

[TemplateDescriptor(
    Name = "Wide Layout",
    Description = "Full-width article layout",
    AvailableWithoutTag = false,
    Tags = new[] { "wide" })]
public class ArticleWideController
    : PageController<ArticlePage>
{
    public IActionResult Index(ArticlePage currentPage)
    {
        return View("IndexWide", currentPage);
    }
}

Editors select the rendering template from the page settings panel.

IssueCauseFix
404 on page requestNo controller found for content typeEnsure controller class name matches {TypeName}Controller
View not foundView in wrong directoryPlace views in Views/{TypeName}/Index.cshtml
Block renders as emptyMissing partial controllerCreate a BlockController<T> for the block type