
Optimizely CMS Controllers and Views: A Step-by-Step Implementation Guide
Building pages in Optimizely CMS can feel overwhelming when you're starting out. You need to understand how models, controllers, and views work together, and the documentation often assumes you already know the basics. This guide walks you through creating your first page type with a custom controller and view, explaining each step along the way.
If you're a .NET developer new to Optimizely CMS 12 or transitioning from an older version, this guide will help you understand the MVC pattern that powers content rendering in Optimizely. You'll learn how to create page types, wire up controllers, build views, and avoid common pitfalls that can derail your first attempts.
Prerequisites
Before you start building controllers and views in Optimizely CMS, make sure you have:
- Visual Studio 2022 or later with ASP.NET and web development workload installed
- .NET 6 or .NET 8 SDK (Optimizely CMS 12 requires .NET 6 minimum)
- An Optimizely CMS 12 project already set up (you can use the Alloy Demo site as a starting point)
- Basic knowledge of C# and ASP.NET Core MVC concepts
- Access to the CMS admin interface (typically at `/episerver/cms` after logging in)
- A valid Optimizely license in your project's root folder as `License.config`
Step-by-Step Implementation
Step 1: Create Your Page Type Model
Start by defining a page type in your `/Models/Pages` folder. This C# class tells Optimizely what content editors can add to this type of page.
using EPiServer.Core;
using EPiServer.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace YourProject.Models.Pages
{
[ContentType(
DisplayName = "Article Page",
GUID = "a8fe12d3-b418-4e6b-9c25-7834f9e2a651",
Description = "Use this page type for articles and blog posts")]
public class ArticlePage : PageData
{
[CultureSpecific]
[Display(
Name = "Article Heading",
Description = "The main heading displayed at the top of the article",
GroupName = SystemTabNames.Content,
Order = 100)]
public virtual string Heading { get; set; }
[CultureSpecific]
[Display(
Name = "Article Body",
Description = "The main content of your article",
GroupName = SystemTabNames.Content,
Order = 200)]
public virtual XhtmlString MainBody { get; set; }
[Display(
Name = "Author Name",
Description = "Who wrote this article",
GroupName = SystemTabNames.Content,
Order = 300)]
public virtual string AuthorName { get; set; }
[Display(
Name = "Publish Date",
Description = "When this article was published",
GroupName = SystemTabNames.Content,
Order = 400)]
public virtual DateTime? PublishDate { get; set; }
}
}Key points about this code:
- The `GUID` must be unique across your entire project
- Properties must be `virtual` for Optimizely's proxy generation to work
- `CultureSpecific` allows different content for different languages
- The `Order` parameter controls the sequence in the editor interface
Step 2: Build Your Controller
Create a controller in the `/Controllers` folder that handles requests for your page type:
using EPiServer.Web.Mvc;
using Microsoft.AspNetCore.Mvc;
using YourProject.Models.Pages;
namespace YourProject.Controllers
{
public class ArticlePageController : PageController
{
public IActionResult Index(ArticlePage currentPage)
{
// Add any business logic here
// For example, format the publish date
if (currentPage.PublishDate == null)
{
currentPage.PublishDate = DateTime.Now;
}
// You can add ViewBag data if needed
ViewBag.ReadingTime = CalculateReadingTime(currentPage.MainBody);
return View(currentPage);
}
private int CalculateReadingTime(XhtmlString content)
{
if (content == null) return 0;
var plainText = content.ToHtmlString();
var wordCount = plainText.Split(' ').Length;
return Math.Max(1, wordCount / 200); // Assuming 200 words per minute
}
}
}The controller name must follow the pattern `{PageTypeName}Controller`. Optimizely uses this naming convention to automatically route requests to the correct controller.
Step 3: Create Your View
Add a view at `/Views/ArticlePage/Index.cshtml`:
@model YourProject.Models.Pages.ArticlePage
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@Model.Heading
@if (!string.IsNullOrEmpty(Model.AuthorName))
{
By @Model.AuthorName
@if (Model.PublishDate.HasValue)
{ @Model.PublishDate.Value.ToString("MMMM d, yyyy") }
@if (ViewBag.ReadingTime > 0)
{ @ViewBag.ReadingTime min read }
}
@Html.PropertyFor(m => m.MainBody)
Notice the use of `@Html.PropertyFor()` for the MainBody property. This helper enables on-page editing in the CMS interface, allowing editors to click directly on content to edit it.
Step 4: Register and Build Your Project
After creating these files, build your project in Visual Studio. If there are no compilation errors, your page type should now be available in the CMS.
Step 5: Test Your Implementation
- Run your project and navigate to the CMS admin interface
- Create a new page using the "Article Page" type you just built
- Fill in the content fields - add a heading, body text, author name, and publish date
- Save and publish the page
- View the published page to see your controller and view in action
Step 6: Add a View Model for Complex Scenarios
When you need to pass additional data beyond the page model, create a view model:
namespace YourProject.Models.ViewModels
{
public class ArticlePageViewModel
{
public ArticlePage CurrentPage { get; set; }
public IEnumerable RelatedArticles { get; set; }
public string Category { get; set; }
public int CommentCount { get; set; }
}
}Update your controller to use the view model:
public class ArticlePageController : PageController
{
private readonly IContentLoader _contentLoader;
public ArticlePageController(IContentLoader contentLoader)
{
_contentLoader = contentLoader;
}
public IActionResult Index(ArticlePage currentPage)
{
var model = new ArticlePageViewModel
{
CurrentPage = currentPage,
RelatedArticles = GetRelatedArticles(currentPage),
Category = DetermineCategory(currentPage),
CommentCount = GetCommentCount(currentPage.ContentLink.ID)
};
return View(model);
}
private IEnumerable GetRelatedArticles(ArticlePage current)
{
// Implementation to fetch related articles
return Enumerable.Empty();
}
private string DetermineCategory(ArticlePage page)
{
// Logic to determine article category
return "Technology";
}
private int GetCommentCount(int pageId)
{
// Fetch comment count from your data source
return 0;
}
}And update your view to use the view model:
@model YourProject.Models.ViewModels.ArticlePageViewModel
@Model.CurrentPage.Heading
@Model.Category
@Html.PropertyFor(m => m.CurrentPage.MainBody)
@if (Model.RelatedArticles.Any()) {
Related Articles
@foreach (var article in Model.RelatedArticles)
{
@article.Heading
}
}
@Model.CommentCount commentsCommon Mistakes to Avoid
1. Incorrect Naming Conventions
The most frequent issue developers encounter is mismatched naming. Your controller must be named exactly `{PageTypeName}Controller`. If your page type is `ArticlePage`, the controller must be `ArticlePageController`, not `ArticleController` or `ArticlePageViewController`.
2. Missing Virtual Keywords
Properties in your page type model must be marked as `virtual`. Without this, Optimizely can't create proxies for your properties:
// Wrong
public string Heading { get; set; }
// Correct
public virtual string Heading { get; set; }3. View Location Problems
Views must be placed in the correct folder structure. For an `ArticlePage`, the view should be at `/Views/ArticlePage/Index.cshtml`, not `/Views/Articles/Index.cshtml` or `/Views/Shared/ArticlePage.cshtml`.
4. Forgetting PropertyFor Helper
Using standard HTML helpers instead of Optimizely's `PropertyFor` disables on-page editing:
@Model.MainBody @Html.PropertyFor(m => m.MainBody)
5. Not Handling Null Values
Our experience shows that forgetting to check for null values is a common source of runtime errors. Always validate optional properties before using them:
// Risky
var wordCount = currentPage.MainBody.ToHtmlString().Split(' ').Length;
// Safe
var wordCount = 0;
if (currentPage.MainBody != null && !string.IsNullOrEmpty(currentPage.MainBody.ToHtmlString()))
{
wordCount = currentPage.MainBody.ToHtmlString().Split(' ').Length;
}6. Ignoring Dependency Injection
Optimizely CMS 12 fully supports dependency injection. Don't create service instances manually in your controllers:
// Wrong
public IActionResult Index(ArticlePage currentPage)
{
var contentLoader = new ContentLoader(); // Don't do this
}
// Correct
private readonly IContentLoader _contentLoader;
public ArticlePageController(IContentLoader contentLoader)
{
_contentLoader = contentLoader;
}Testing and Verification Steps
Step 1: Verify Page Type Registration
After building your project, check that your page type appears in the CMS:
- Log into the CMS admin interface
- Click "Create new page"
- Your "Article Page" should appear in the list of available page types
Step 2: Test Content Creation
Create a test page with your new page type:
- Fill in all fields with sample data
- Save the page (don't publish yet)
- Preview the page to verify rendering
- Check that all properties display correctly
Step 3: Validate On-Page Editing
- Navigate to your published page while logged into the CMS
- Hover over content areas marked with `PropertyFor`
- Verify that edit buttons appear
- Click to edit and ensure changes save correctly
Step 4: Check Error Handling
Test your page with edge cases:
- Create a page with minimal content (only required fields)
- Leave optional fields empty
- Add very long content to text fields
- Test with special characters in text fields
Step 5: Performance Verification
We've found that page load time is crucial for user experience. Test your implementation:
- Use browser developer tools to check page load time
- Verify that no unnecessary database calls are made
- Check that view models don't fetch more data than needed
- Monitor memory usage during page rendering
Best Practices and Recommendations
Use Feature Folders
Instead of separating models, views, and controllers into different root folders, consider organizing by feature:
/Features
/ArticlePage
ArticlePage.cs
ArticlePageController.cs
Index.cshtml
/ProductPage
ProductPage.cs
ProductPageController.cs
Index.cshtmlImplement Caching Strategically
Add output caching to improve performance:
[ResponseCache(Duration = 600, VaryByQueryKeys = new[] { "category", "page" })]
public IActionResult Index(ArticlePage currentPage)
{
// Controller logic
}Create Base Classes for Common Functionality
Build a base page type for shared properties:
public abstract class SitePageData : PageData
{
public virtual string MetaDescription { get; set; }
public virtual string MetaKeywords { get; set; }
public virtual bool HideFromSearch { get; set; }
}
public class ArticlePage : SitePageData
{
// Article-specific properties
}Use Strongly-Typed Configuration
Define settings in a strongly-typed manner:
public class ArticleSettings
{
public int MaxRelatedArticles { get; set; } = 5;
public bool EnableComments { get; set; } = true;
public int PreviewLength { get; set; } = 200;
}Conclusion
Building controllers and views in Optimizely CMS follows familiar ASP.NET Core patterns, but requires attention to naming conventions and Optimizely-specific helpers. Start with simple page types and gradually add complexity through view models and dependency injection as your requirements grow.
The key to success is understanding the relationship between page types, controllers, and views. Once you grasp how Optimizely routes requests based on naming conventions and how the `PropertyFor` helper enables editing, you can build sophisticated content experiences.
Working with teams has taught us that the most successful Optimizely implementations start simple and iterate. Don't try to build complex view models and elaborate controller logic on your first attempt. Get a basic page working, test it thoroughly, then add features incrementally.
Need help implementing custom page types and controllers in Optimizely CMS? If you're planning a content architecture that requires specific rendering logic or complex view models, we can help you design a maintainable structure that scales with your content needs. Our team can review your requirements and suggest implementation patterns that align with Optimizely best practices while meeting your specific business goals.
