Install
npx skillscat add twofoldtech-dakota/claude-marketplace/frontend-razor Install via the SkillsCat registry.
SKILL.md
Frontend Razor Patterns
Overview
This skill covers Razor view patterns for Optimizely CMS including page templates, block rendering, and content area handling.
Page Templates
Basic Page View
@model ArticlePage
@{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewData["Title"] = Model.MetaTitle ?? Model.Heading;
}
<article class="article">
<header class="article-header">
<h1>@Model.Heading</h1>
@if (Model.PublishedDate.HasValue)
{
<time datetime="@Model.PublishedDate.Value.ToString("yyyy-MM-dd")">
@Model.PublishedDate.Value.ToString("MMMM d, yyyy")
</time>
}
</header>
@if (Model.HeroImage != null)
{
<figure class="article-hero">
<img src="@Url.ContentUrl(Model.HeroImage)" alt="@Model.Heading" />
</figure>
}
<div class="article-content">
@Html.PropertyFor(m => m.MainBody)
</div>
@Html.PropertyFor(m => m.RelatedContentArea)
</article>View with ViewModel
@model ArticleViewModel
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<article class="article">
<h1>@Model.Heading</h1>
@Html.Raw(Model.Body)
<section class="related-articles">
<h2>Related Articles</h2>
<ul>
@foreach (var related in Model.RelatedArticles)
{
<li>
<a href="@related.Url">@related.Heading</a>
</li>
}
</ul>
</section>
</article>Block Templates
Block View
@model HeroBlock
<section class="hero" style="background-image: url('@Url.ContentUrl(Model.BackgroundImage)')">
<div class="hero-content">
<h1>@Model.Heading</h1>
@if (!string.IsNullOrEmpty(Model.Subheading))
{
<p class="hero-subheading">@Model.Subheading</p>
}
@if (Model.CallToActionUrl != null)
{
<a href="@Model.CallToActionUrl" class="btn btn-primary">
@(Model.CallToActionText ?? "Learn More")
</a>
}
</div>
</section>Block with Edit Mode Support
@using EPiServer.Web.Mvc.Html
@model TeaserBlock
<div class="teaser">
@Html.PropertyFor(m => m.Heading, new { Tag = "h3", CssClass = "teaser-heading" })
@Html.PropertyFor(m => m.Text, new { CssClass = "teaser-text" })
@if (Model.Link != null)
{
<a href="@Url.ContentUrl(Model.Link)" class="teaser-link">
@Html.PropertyFor(m => m.LinkText)
</a>
}
</div>Content Areas
Rendering Content Areas
@using EPiServer.Web.Mvc.Html
@model PageData
<main class="page-content">
@* Default rendering *@
@Html.PropertyFor(m => m.MainContentArea)
@* With custom tag and CSS class *@
@Html.PropertyFor(m => m.SidebarArea, new {
Tag = "aside",
CssClass = "sidebar",
ChildrenTag = "div",
ChildrenCssClass = "sidebar-block"
})
@* With custom item wrapper *@
@Html.PropertyFor(m => m.FooterArea, new {
CustomTag = "section",
CssClass = "footer-blocks"
})
</main>Content Area with Custom Rendering
@using EPiServer.Core
@model ContentArea
@if (Model != null && Model.FilteredItems.Any())
{
<div class="content-blocks">
@foreach (var item in Model.FilteredItems)
{
var content = item.GetContent();
<div class="content-block @GetBlockClass(content)">
@Html.DisplayFor(m => content)
</div>
}
</div>
}
@functions {
private string GetBlockClass(IContent content)
{
return content.GetOriginalType().Name.ToLowerInvariant().Replace("block", "-block");
}
}Tag Helpers
Optimizely Tag Helpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, EPiServer.Web.Mvc
@* Content link tag helper *@
<a epi-link="@Model.LinkToPage">@Model.LinkText</a>
@* Property editing *@
<div epi-edit="MainBody">
@Html.Raw(Model.MainBody?.ToHtmlString())
</div>
@* Content URL *@
<img src="@Url.ContentUrl(Model.Image)" alt="@Model.ImageAlt" />Custom Tag Helper
[HtmlTargetElement("optimizely-breadcrumb")]
public class BreadcrumbTagHelper : TagHelper
{
private readonly IContentLoader _contentLoader;
private readonly IUrlResolver _urlResolver;
public ContentReference CurrentPage { get; set; }
public BreadcrumbTagHelper(
IContentLoader contentLoader,
IUrlResolver urlResolver)
{
_contentLoader = contentLoader;
_urlResolver = urlResolver;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "nav";
output.Attributes.Add("aria-label", "Breadcrumb");
var breadcrumbs = GetBreadcrumbs(CurrentPage);
var list = new StringBuilder("<ol class=\"breadcrumb\">");
foreach (var crumb in breadcrumbs)
{
list.Append($"<li><a href=\"{crumb.Url}\">{crumb.Name}</a></li>");
}
list.Append("</ol>");
output.Content.SetHtmlContent(list.ToString());
}
}Partial Views
Shared Partial
@* Views/Shared/_ArticleCard.cshtml *@
@model ArticleViewModel
<article class="article-card">
@if (!string.IsNullOrEmpty(Model.ThumbnailUrl))
{
<img src="@Model.ThumbnailUrl" alt="" class="article-card-image" />
}
<div class="article-card-content">
<h3 class="article-card-title">
<a href="@Model.Url">@Model.Heading</a>
</h3>
<p class="article-card-excerpt">@Model.Excerpt</p>
<time datetime="@Model.PublishedDate?.ToString("yyyy-MM-dd")">
@Model.PublishedDate?.ToString("MMM d, yyyy")
</time>
</div>
</article>Using Partial
@model ArticleListPage
<div class="article-list">
@foreach (var article in Model.Articles)
{
@await Html.PartialAsync("_ArticleCard", article)
}
</div>Edit Mode Support
Edit Mode Detection
@using EPiServer.Web
@model PageData
@if (PageEditing.PageIsInEditMode)
{
<div class="edit-mode-notice">
You are in edit mode
</div>
}
<div class="content" data-epi-edit="@Html.EditAttributes(m => m.MainBody)">
@Html.PropertyFor(m => m.MainBody)
</div>Best Practices
- Use PropertyFor for editable properties
- Support edit mode with proper attributes
- Use partials for reusable components
- Separate views and viewmodels for complex pages
- Use tag helpers for clean markup
- Handle null content gracefully