Render an experience with tag helpers
Describes how to render Visual Builder experiences using tag helpers or HTML helpers in Optimizely Content Management System (CMS 13).
An experience is a page-level composition created in the Visual Builder. It organizes content into a tree of sections, rows, columns, and components. Optimizely CMS provides a set of tag helpers for rendering experiences declaratively in Razor views.
NoteMicrosoft is no longer investing in the Html Helpers technology for building templates, and they have removed the related documentation from recent versions. You should also be aware that Microsoft may eventually discontinue support for Html Helper technology altogether.
Prerequisites
-
Install the
EPiServer.CMS.AspNetCore.TagHelperspackage. -
Register tag helper dependencies in
Startup.cs:services.AddCmsTagHelpers(); -
Add the tag helper directive to
Views/_ViewImports.cshtml:@addTagHelper *, EPiServer.Cms.AspNetCore.TagHelpers
NoteFor working with Visual Builder pages and on-page editing (OPE), see Render properties with tag helpers.
Experience composition structure
An experience consists of a tree of composition nodes:
Experience (root)
+-- Section (grid layout)
| +-- Row
| | +-- Column
| | +-- Component (a block/content item)
| +-- Row
| +-- Column
| +-- Component
+-- Component (standalone, outside a grid)
The following tag helpers correspond to each node type:
| Tag Helper | Default HTML | Description |
|---|---|---|
<epi-outline> | <div> | Root wrapper. Renders the entire experience composition. |
<epi-grid> | <div> | Renders a section node with grid layout. |
<epi-row> | <div> | Renders a row inside a grid. |
<epi-column> | <div> | Renders a column inside a row. |
<epi-component> | <div> | Renders a component (block) within the composition. |
<epi-styles> | None | Declares CSS class mappings for display templates. |
<epi-style> | None | Maps a display setting name/value pair to CSS classes. |
<epi-style-map> | None | Alternative mapping syntax inside <epi-style>. |
Render an experience with tag helpers
The simplest way to render an experience is to use <epi-outline> with the grid tag helpers nested inside:
@model IPageViewModel<ExperienceData>
<epi-outline class="experience" experience="@Model.CurrentPage">
<epi-grid>
<epi-row>
<epi-column>
<epi-component />
</epi-column>
</epi-row>
</epi-grid>
<epi-component />
</epi-outline>The experience attribute accepts a model expression pointing to your ExperienceData. If the model is null, the tag helper renders nothing.
How the rendering works:
<epi-outline>converts theExperienceDatainto aCompositiontree and iterates over top-level nodes.- For each section node, it tries to resolve a Razor partial view (display template) for the section.
- If the framework finds none, the renderer falls back to the tag helper template declared inside
<epi-outline>(the<epi-grid>block). - If no inline template exists, the framework renders the default grid layout.
- If the framework finds none, the renderer falls back to the tag helper template declared inside
- For each component node, it renders the component using the standard Optimizely template resolution, honoring the
DisplayTemplateKeyfrom the composition node. - In edit mode,
data-epi-block-idattributes are automatically added to enable Visual Builder editing. No additional markup is needed.
Customize the HTML tag
All composition node tag helpers render as <div> by default. Use the tag-name attribute to change the output element:
<epi-outline tag-name="section" experience="@Model.CurrentPage">
<epi-grid tag-name="main">
<epi-row tag-name="section">
<epi-column tag-name="article">
<epi-component tag-name="aside" />
</epi-column>
</epi-row>
</epi-grid>
</epi-outline>Display templates
The rendering engine uses the standard Optimizely template resolution to find views for sections and components. When a node has a DisplayTemplateKey, the framework tries to find an MVC partial view tagged with that key. For a detailed explanation of how the TemplateResolver selects templates based on content type, tags, and inheritance, see Select templates.
Section templates
- The renderer checks if the section's
DisplayTemplateKeyis set. - If set, it resolves a partial view registered with that tag (for example, via
[TemplateDescriptor]). - If a matching view is found, it renders that view with the section's
SectionDataas the model. - If no matching view is found, it falls back to the inline tag helper template (
<epi-grid>block inside<epi-outline>). - If no inline template is declared, the framework renders a built-in default grid layout..
To create a section display template, add a Razor partial view for your SectionData type.
[ContentType]
public class MySection : SectionData
{
[Display(GroupName = SystemTabNames.Content)]
[CultureSpecific]
public virtual string Title { get; set; }
[Display(Name = "Short description", GroupName = SystemTabNames.Content)]
public virtual string Description { get; set; }
}Then create the corresponding Razor view:
@* Views/Shared/MySection.cshtml *@
@model MySection
<epi-grid class="container section">
<h4 epi-property="@Model.Title">@Model.Title</h4>
<p epi-property="@Model.Description">@Model.Description</p>
<epi-styles>
<epi-style name="margin">
<epi-style-map value="top" class="mt-3"/>
<epi-style-map value="bottom" class="mb-5"/>
<epi-style-map value="both" class="mt-3 mb5"/>
</epi-style>
</epi-styles>
<epi-row class="row">
<epi-column class="col-sm-12 col-md mb-4 mb-md-0">
<epi-component/>
</epi-column>
</epi-row>
</epi-grid>The framework resolves the view automatically by convention (matching the content type name). You can also register additional section templates explicitly using IViewTemplateModelRegistrator (see Component templates below).
View file naming convention for display tags
In addition to the default view ({ContentTypeName}.cshtml), you can create tag-specific views by appending the display tag name to the content type name with a dot separator:
{ContentTypeName}.{DisplayTag}.cshtml
When the framework renders a component or section, it checks for a display tag (from the DisplayTemplateKey or the view context). If a tag is present, the renderer first looks for a view named {ContentTypeName}.{DisplayTag}.cshtml. If no matching view is found, it falls back to the default {ContentTypeName}.cshtml view.
For example, given a BlankSection content type and a display tag CardSection, create a view file named BlankSection.CardSection.cshtml:
@* Views/Shared/BlankSection.CardSection.cshtml *@
@model BlankSection
<epi-grid class="container section card">
<epi-styles>
<epi-style name="margin">
<epi-style-map value="top" class="mt-3"/>
<epi-style-map value="bottom" class="mb-5"/>
<epi-style-map value="both" class="mt-3 mb5"/>
</epi-style>
<epi-style name="color">
<epi-style-map value="primary" class="primary"/>
<epi-style-map value="secondary" class="secondary"/>
</epi-style>
</epi-styles>
<epi-row class="row">
<div class="col-sm-12 col-md mb-4 mb-md-3">
<epi-column class="card">
<epi-styles>
<epi-style name="color" value="primary" class="primary"/>
<epi-style name="color" value="secondary" class="secondary"/>
</epi-styles>
<epi-component/>
</epi-column>
</div>
</epi-row>
</epi-grid>This view renders when a BlankSection is assigned the display template key "CardSection" in the Visual Builder, while the default BlankSection.cshtml renders when no display tag is set.
You can create multiple tagged views for the same content type without any code registration:
Views/Shared/
├── BlankSection.cshtml ← default (no tag)
├── BlankSection.CardSection.cshtml ← tag "CardSection"
└── BlankSection.FullWidth.cshtml ← tag "FullWidth"
NoteThis naming convention works alongside
IViewTemplateModelRegistrator. If you need additional control (such as settingAvailableWithoutTagor specifying multiple tags for the same view), use the programmatic registration approach described in the Component templates section.
Component templates
Components follow the same template resolution pattern. Each ComponentNode has a Component property (the actual IContentData block) and an optional DisplayTemplateKey.
- If
DisplayTemplateKeyis set, the framework resolves a partial view tagged with that key for the component's content type. - The matched view is rendered with the block as the model.
- If no tagged view is found, the framework uses the standard default view.
The default view is resolved by convention from its filename matching the content type name:
@* Views/Shared/Blocks/TeaserBlock.cshtml (default template) *@
@model TeaserBlock
<div>
<div class="img-wrapper mb-3" epi-property="@Model.Image">
<img epi-property="@Model.Image" />
</div>
<h3 epi-property="@Model.Heading">@Model.Heading</h3>
<p epi-property="@Model.Text">@Model.Text</p>
</div>A wide variant is defined in a separate view file:
@* Views/Shared/Blocks/TeaserBlockWide.cshtml (wide template) *@
@model TeaserBlock
<div class="row align-items-center">
<div class="col-sm-12 col-lg-6" epi-property="@Model.Image">
<img class="pb-3 pb-lg-0" epi-property="@Model.Image" />
</div>
<div class="col-sm-12 col-lg-6 text-lg-start">
<h2 epi-property="@Model.Heading">@Model.Heading</h2>
<p epi-property="@Model.Text">@Model.Text</p>
</div>
</div>Register the wide template with tags using IViewTemplateModelRegistrator in a TemplateCoordinator:
[ServiceConfiguration(typeof(IViewTemplateModelRegistrator))]
public class TemplateCoordinator : IViewTemplateModelRegistrator
{
public void Register(TemplateModelCollection viewTemplateModelRegistrator)
{
viewTemplateModelRegistrator.Add(typeof(TeaserBlock), new TemplateModel
{
Name = "TeaserBlockWide",
Tags = ["wide", "full"],
AvailableWithoutTag = false,
});
}
}When a component in the Visual Builder is assigned the display template key "wide", the TeaserBlockWide partial is rendered instead of the default.
Style display settings with CSS classes
Display settings are key-value pairs configured in the Visual Builder (for example, margin=top, padding=large). The style tag helpers let you map these values to CSS classes declaratively.
Declare a style section with <epi-styles>
<epi-styles>Use <epi-styles> inside <epi-outline> (or any composition tag helper) to declare CSS classes and display setting mappings. It produces no HTML output. It only configures styling for the composition nodes.
| Attribute | Required | Description |
|---|---|---|
name | No | The DisplayTemplateKey this style section applies to. If omitted, applies as the default fallback. |
class | No | CSS classes to always add to nodes matching this template. |
<epi-outline experience="@Model.CurrentPage">
<!-- Default styles (fallback when no named match is found) -->
<epi-styles class="experience" />
<!-- Styles for the "dark" display template -->
<epi-styles name="dark" class="experience dark-theme" />
<epi-grid>
<epi-row>
<epi-column>
<epi-component />
</epi-column>
</epi-row>
</epi-grid>
</epi-outline>Fallback behavior: When a composition node has a DisplayTemplateKey of "dark", the <epi-styles name="dark"> section is used. If a node has no template key, or its key does not match any named <epi-styles>, the unnamed (default) section is used as a fallback.
Map display settings with <epi-style>
<epi-style>Use <epi-style> inside <epi-styles> to map specific display setting values to CSS classes. You can use self-closing syntax for single mappings:
<epi-styles class="experience">
<epi-style name="padding" value="none" class="p-0" />
<epi-style name="padding" value="small" class="p-3" />
<epi-style name="padding" value="large" class="p-9" />
<epi-style name="margin" value="top" class="mt-3" />
<epi-style name="margin" value="bottom" class="mb-3" />
<epi-style name="margin" value="both" class="mt-3 mb-3" />
</epi-styles>| Attribute | Required | Description |
|---|---|---|
name | Yes | The display setting name to match (for example, "padding"). |
value | Yes (self-closing mode) | The display setting value to match (for example, "small"). |
class | Yes (self-closing mode) | Space-separated CSS classes to apply when matched. |
Group mappings with <epi-style-map>
<epi-style-map>For grouping multiple value mappings under a single display setting name, use <epi-style> in normal (non-self-closing) mode with <epi-style-map> children:
<epi-styles class="experience">
<epi-style name="margin">
<epi-style-map value="top" class="mt-3" />
<epi-style-map value="bottom" class="mb-3" />
<epi-style-map value="both" class="mt-3 mb-3" />
</epi-style>
<epi-style name="padding">
<epi-style-map value="none" class="p-0" />
<epi-style-map value="small" class="p-3" />
<epi-style-map value="large" class="p-9" />
</epi-style>
</epi-styles>Both syntaxes produce identical results.
Per-template style mappings
You can define different CSS mappings for different display templates:
<epi-outline experience="@Model.CurrentPage">
<!-- Default template: standard spacing -->
<epi-styles class="experience">
<epi-style name="margin" value="top" class="mt-3" />
<epi-style name="margin" value="bottom" class="mb-3" />
</epi-styles>
<!-- "dark" template: larger spacing -->
<epi-styles name="dark" class="experience dark-theme">
<epi-style name="margin" value="top" class="mt-5" />
<epi-style name="margin" value="bottom" class="mb-5" />
</epi-styles>
<epi-grid>
<epi-row>
<epi-column>
<epi-component />
</epi-column>
</epi-row>
</epi-grid>
</epi-outline>When a node has DisplayTemplateKey = "dark", the framework applies the CSS classes from the name="dark" styles section. For nodes without a matching template key, the unnamed default section is used. If the same name appears in multiple <epi-styles> blocks, the framework merges their mapping rules.
Render an experience with HTML helpers
For full control over the markup, you can bypass tag helpers and render the composition tree manually. This approach uses ICompositionMapper to convert ExperienceData to Composition model to access the composition tree.
NoteYou should use the tag helper approach for most scenarios because it handles edit mode attributes, template resolution, and style mapping automatically. Use the manual approach when you need complete control over the HTML structure.
Manual rendering example
The following example demonstrates manual rendering:
@using EPiServer.Cms.VisualBuilder.Rendering
@using EPiServer.VisualBuilder.Compositions
@model ExperienceViewModel<HtmlHelperExperience>
@{
var marginMap = new Dictionary<string, string>
{
{ "top", "mt-3" },
{ "bottom", "mb-3" },
{ "both", "mt-3 mb-3" }
};
}
@if (Model.Composition != null)
{
<div class="experience @Model.Composition.GetTheme()">
@foreach (var section in Model.Composition.Nodes)
{
<div class="container section @(section.GetCss("margin", marginMap))"
@Html.EditAttributes(section)>
@if (section is SectionNode sectionNode)
{
foreach (var row in sectionNode.Nodes)
{
<div class="row @row.GetCss("margin", marginMap)"
@Html.EditAttributes(row)>
@if (row is StructureNode rowNode)
{
foreach (var column in rowNode.Nodes)
{
<div class="col-sm-12 col-md mb-4 mb-md-0
@column.GetCss("margin", marginMap)"
@Html.EditAttributes(column)>
@if (column is StructureNode columnNode)
{
foreach (var component in columnNode.Nodes)
{
<div @Html.EditAttributes(component)>
@if (component is ComponentNode componentNode)
{
await Html.RenderContentDataAsync(
componentNode.Component,
false,
[componentNode.DisplayTemplateKey],
new { Node = componentNode });
}
</div>
}
}
</div>
}
}
</div>
}
}
else if (section is ComponentNode componentNode)
{
await Html.RenderContentDataAsync(
componentNode.Component,
false,
[componentNode.DisplayTemplateKey],
new { Node = componentNode });
}
</div>
}
</div>
} public static string GetTheme(this CompositionNode model)
{
if (model.DisplayTemplateKey == "MyExperience")
{
return "cool-experience";
}
return "";
}
public static string? GetCss(this CompositionNode node, string key, Dictionary<string, string> map, string? defaultCss = null)
{
if (node.DisplaySettings.TryGetValue(key, out var setting))
{
if (map.TryGetValue(setting, out var css))
{
return css;
}
}
return defaultCss;
}
Key points for manual rendering
- Use
@Html.EditAttributes(node)on every container element to support Visual Builder editing. - Use
Html.RenderContentDataAsync()to render individual components, passing theDisplayTemplateKeyas a tag.
Updated 9 days ago
