Listings and navigation
Introduces some options for building content navigation in Optimizely Content Management System (CMS).
You can, for example, build navigation using link collections, structure-based visual components like menus and breadcrumbs, or search-based navigation and filtering.
Link Collections
A Link Collection is often used for things like related content or lists of links in the page footer. Since link collections are editorial content, this data must be stored in a user-defined property.
In this example, you add a property of the type LinkItemCollection
 to a content type named ArticlePage
to enable editors to build a list of links that can be internal or external. You could also use the type IList<ContentReference>
instead of LinkItemCollection
if you only reference internal content.
[ContentType(...)]
public class ArticlePage: PageData {
[CultureSpecific]
public virtual string TeaserText {
get;
set;
}
[CultureSpecific]
public virtual XhtmlString MainBody {
get;
set;
}
[CultureSpecific]
public virtual LinkItemCollection RelatedContentLinks {
get;
set;
}
}
Add a page controller and then use Html.PropertyFor
to render the list of links in your page template. The default rendering for LinkItemCollection
is an unordered list using <ul>
, <li>
, and <a>
tags.
@model EpiDemo.Models.Pages.ArticlePage
<!DOCTYPE html>
<html>
<head>
<title>@Model.Name</title>
</head>
<body>
<h1>@Html.PropertyFor(m => m.Name, new { customTag = "span" })</h1>
@Html.PropertyFor(m => m.MainBody)
<h2>Releated Content</h2>
@Html.PropertyFor(m => m.RelatedContentLinks)
</body>
</html>
Custom Rendering of Link Collections
You can fully control rendering when the built-in way does not work. As a simple example, you can replace PropertyFor
 in the example above with your logic, and iterate the collection yourself in the View.
...
@Html.PropertyFor(m => m.MainBody)
// FullRefreshPropertiesMetaData asks on-page edit to reload the page
// to run the following custom rendering again after the value has changed.
@Html.FullRefreshPropertiesMetaData(new []{ "RelatedContentLinks" })
// EditAttributes enables on page-edit when you have custom rendering.
<p @Html.EditAttributes(m => m.CurrentPage.RelatedContentLinks) >
@if (Model.CurrentPage.RelatedContentLinks != null)
{
<span>See also:</span>
foreach (LinkItem item in Model.CurrentPage.RelatedContentLinks)
{
<a href="@UrlResolver.Current.GetUrl(item.Href)">@item.Text</a> }
}
</p>
...
Note
Avoid adding custom rendering of properties directly into your page templates. When you do, make sure you use
EditAttributes
andFullRefreshPropertiesMetaData
. We strongly recommend that you learn how to useDisplayTemplates
andUIHint
as described below instead. This will make it easier for editors to work with your templates, since it keeps the on-page editing experience working smoothly.
A better way to build custom rendering is to move it into a Display Template. Add a partial template with the name SeeAlso.cshtml
 in your DisplayTemplates
folder (/Shared/DisplayTemplates/SeeAlso.cshtml
), and move your custom rendering from your page template. Set the model to be the same type as the property type.
@using EPiServer.SpecializedProperties
@model LinkItemCollection
@if (Model != null) {
<p>
See also:
@foreach (LinkItem item in Model) {
<a href="@item.Href">@item.Text</a>
}
</p>
}
Then, restore your rendering in your page template to use PropertyFor
 with an extra parameter that contains the name of the display template. On-page editing will work as expected and preview will be correct without forced page reloads.
...
@Html.PropertyFor(m => m.MainBody)
<h2>Releated Content</h2>
@Html.PropertyFor(m => m.CurrentPage.RelatedContentLinks, "SeeAlso")
...
Alternatively, add a UIHint
 attribute to your property definition in your content model and specify the name of your display template for custom rendering.
public class ArticlePage: PageData {
...
[UIHint("SeeAlso")]
public virtual LinkItemCollection RelatedContentLinks {
get;
set;
}
...
Now, whenever you use Html.PropertyFor
to render this property in a template, it uses your custom rendering by default.
Link Collections in Layout
If you want to use a Link Collection in, for example, your page footer, you can use a similar approach. The main difference is that you no longer want this data stored on each page; instead, you want to edit it once and see the page footer updated on all pages.
Common patterns to accomplish this to add a user-defined property to the site's start page or to have a special content type just for site settings. You then load this content item and put it into your view model and use it from your layout.
Structure-based navigation
There are many ways to build website navigations. A common method in CMS is using the page hierarchy to construct lists and navigation. For the Alloy sample site, the Top menu is constructed from all the children of the start page. Breadcrumbs renders a list of all ancestors (parents) of a page in the hierarchy up to the start page. A Hierarchical Menu (or Submenu) mirrors a part of the page hierarchy. A List of Pages is often created by listing all children of a specific page or through a search. There are also data types that can be used to allow editors to construct arbitrary collection of links.
Menus can be built programmatically using collections of pages gathered through GetPage(ContentReference)
, GetChildren(ContentReference)
and other methods of the IContentLoader
service. When you have retrieved an instance of a page, you have access to all values both user defined and build in.
Display in navigation
All content that inherits from PageData
has a built-in Boolean property named VisibleInMenu
. The intended use of this property is to enable an editor to hide pages from menus and breadcrumbs. There is no built-in logic for how this should work in your templates built; it is up to you as a developer to implement this functionality. There are a few examples below how this can be done.
Create a page listing
To illustrate listings, we start by creating a simple list of pages that are children to the current page.
There are several ways of passing data to views. A view model is useful if you want to pass typed data beyond your content into your views. You can define a model type in the view, and then pass an instance of this type to the view from the action method.
We use a simple class in this example but in a real-world site, it is common to use base classes, interfaces, and generic types to make it easier to access the view model from your shared layout views. See Content model and views.
public class ArticlePageViewModel {
public IEnumerable < ArticlePage > ListOfArticles {
get;
set;
}
public ArticlePage CurrentPage {
get;
set;
}
}
Add the following code to your page controller to load all child pages below the current page and create a view model.
public ActionResult Index(ArticlePage currentPage) {
var repo = ServiceLocator.Current.GetInstance < IContentLoader > ();
// Load all article pages that are direct children to the current page.
var pages = repo.GetChildren < ArticlePage > (currentPage.ContentLink);
// TODO: Add filter to hide unpublished pages and apply access control.
var model = new ArticlePageViewModel {
CurrentPage = currentPage,
ListOfArticles = pages
};
return View(model);
}
Note
It is good practice to use constructor injection but that requires some additional setup in your project first, so we use the Service Locator pattern to resolve dependencies in this example.
GetPage
and GetChildren
expect an ID parameter implemented by the ContentReference
class. You can find the current page ID in the property ContentLink
, and the property ParentLink
contains the ID of the parent page. The reason you have a class for the ID and not just a simple integer is to be able to load previously published versions and drafts through the same methods.
You can filter the type of content you retrieved with GetChildren
 by using a different class on the generic method. So, PageData
 instead of ArticlePage
 in the call above gives you a list of any type of page, and IContent
 retrieves any type of content, including media, folders, and blocks.
The next step is to use the view model in your view to render the list of articles.
@using EPiServer.Core
@using EpiDemo.Models.Pages
@model EpiDemo.Models.ViewModels.ArticlePageViewModel
<!DOCTYPE html>
<html>
<head>
<title>@Model.CurrentPage.Name</title>
</head>
<body>
<h1>@Html.PropertyFor(m => m.CurrentPage.Name, new { customTag = "span" })</h1>
@Html.PropertyFor(m => m.CurrentPage.MainBody)
<h2>List of Articles</h2>
@foreach (ArticlePage item in Model.ListOfArticles) {
<h3>@Html.ContentLink(item) <small>by @item.CreatedBy</small></h3>
<p>@item.TeaserText</p>
}
</body>
</html>
Configure location of list
You can, of course, load other pages than the children of the current page. Here is an example that lets an editor pick the parent page for the list of pages by adding a property to our page type.
public class ArticlePage: PageData {
...
public virtual ContentReference ParentForList {
get;
set;
}
...
Then use this property as ID when you load the list of pages in your controller.
public ActionResult Index(ArticlePage currentPage) {
var repo = ServiceLocator.Current.GetInstance < IContentLoader > ();
// Load all article pages that are direct children to the page specified by
// the ParentForList property.
var pages = repo.GetChildren < ArticlePage > (currentPage.ParentForList);
...
Filter content for access
Pages retrieved using calls to, for example, GetChildren
 are not automatically filtered for access and availability.
Important
As a developer you are always responsible to add functionality for filtering content you have retrieved through Optimizely's API before you use it in a view. Forgetting this can cause broken links and you could also reveal sensitive information to unauthorized visitors of your site.
There are several utility classes to help you filter content. The first class you must learn how to use is FilterContentForVisitor
. It uses three other filters to remove unpublished pages (FilterPublished
), pages without templates (FilterTemplate
), and pages to which the current visitor does not have access (FilterAccess
).
public ActionResult Index(ArticlePage currentPage) {
var repo = ServiceLocator.Current.GetInstance < IContentLoader > ();
// Create filter to remove unpublished pages and apply access control.
var filter = new FilterContentForVisitor();
// Load all article pages that are direct children to the current page and apply filter.
var pages = repo.GetChildren < ArticlePage > (currentPage.ContentLink)
.Where(page => !filter.ShouldFilter(page));
var model = new ArticlePageViewModel {
CurrentPage = currentPage,
ListOfArticles = pages
};
return View(model);
}
You will also see the static class FilterForVisitor
in many examples. This legacy class uses FilterContentForVisitor
but because it is static it makes unit testing harder to perform.
To learn more, see Search and filter.
Page listings in menu navigation
Top and left navigation menus are common examples of site navigation components. To make the page list useful for navigation purposes, you must make it available from all pages on the site since navigation is usually rendered from shared layout templates.
One way to solve this is to let your controller prepare your view model with data needed by the shared layout, like metadata, menu items, breadcrumbs, and footer links. Another approach is to use helper methods directly in your shared views to retrieve what you need.
The following example shows an HtmlHelper
 extension method named MenuList
that takes two parameters. The first is the ID of the parent page, and the second is an HTML template. It gets children of the parent page that should be visible to the visitor and renders the template. Selected is true if the Menu Item is in the path to the current page, and that is often used to highlight where you are in the structure.
public static class HtmlHelperExtensions {
public static IHtmlContent MenuList(
this IHtmlHelper helper,
ContentReference rootLink,
Func < MenuItem, HelperResult > itemTemplate) {
var currentContentLink = helper.ViewContext.HttpContext.GetContentLink();
var contentLoader = helper.ViewContext.HttpContext.RequestServices.GetService < IContentLoader > ();
var filterForVisitor = new FilterContentForVisitor();
var pagePath = contentLoader.GetAncestors(currentContentLink)
.Reverse()
.Select(x => x.ContentLink)
.SkipWhile(x => !x.CompareToIgnoreWorkID(rootLink))
.ToList();
var menuItems = contentLoader.GetChildren < PageData > (rootLink)
.Where(page => !filterForVisitor.ShouldFilter(page) && page.VisibleInMenu)
.Select(page => new MenuItem {
Page = page,
Selected = page.ContentLink.CompareToIgnoreWorkID(currentContentLink) ||
pagePath.Contains(page.ContentLink)
})
.ToList();
var buffer = new StringBuilder();
var writer = new StringWriter(buffer);
foreach(var menuItem in menuItems) {
itemTemplate(menuItem).WriteTo(writer, HtmlEncoder.Default);
}
return new HtmlString(buffer.ToString());
}
public class MenuItem {
public PageData Page {
get;
set;
}
public bool Selected {
get;
set;
}
}
}
Use the MenuList
extension method in your page layout to render a top menu with the start page as first item followed by all children directly below the start page.
@helper TopMenuTemplate(HtmlHelperExtensions.MenuItem menuItem) {
<li>
@if (menuItem.Selected) {
<strong>@Html.PageLink(menuItem.Page)</strong>
}
else {
@Html.PageLink(menuItem.Page)
}
</li>
}
<nav>
<ul>
<li>
@Html.PageLink(ContentReference.StartPage)
</li>
@Html.MenuList(ContentReference.StartPage, TopMenuTemplate)
</ul>
</nav>
You can also use MenuList
to render hierarchical navigation by calling it recursively in your template.
@helper SubMenuTemplate(HtmlHelperExtensions.MenuItem menuItem) {
<li>
@if (menuItem.Selected) {
<strong>@Html.PageLink(menuItem.Page)</strong>
<ul>
@Html.MenuList(menuItem.Page.ContentLink, SubMenuTemplate)
</ul>
}
else {
@Html.PageLink(menuItem.Page)
}
</li>
}
<nav>
<ul>
@Html.MenuList(ContentReference.StartPage, SubMenuTemplate)
</ul>
</nav>
To learn more, look at how the Alloy demo templates were built.
Breadcrumbs
A breadcrumb with the path to the current page can be created by using GetAnscestors
that returns a list of parents from the specified page to the root page.
Here is an example of a partial template that renders breadcrumbs. It applies several filters to get rid of unwanted items.
@using EPiServer
@using EPiServer.Core
@using EPiServer.Filters
@using EPiServer.ServiceLocation
@using EPiServer.Web.Routing
@{
var loader = ServiceLocator.Current.GetInstance<IContentLoader>();
var pageRouteHelper = ServiceLocator.Current.GetInstance<IPageRouteHelper>();
var currentPage= pageRouteHelper.Page;
var filter = new FilterContentForVisitor();
var breadcrumbs = loader.GetAncestors(pageRouteHelper.PageLink)
.OfType<PageData>()
.Reverse()
.SkipWhile(page => page.ContentLink != ContentReference.StartPage)
.Where(page => !filter.ShouldFilter(page) && page.VisibleInMenu)
.ToList();
}
<div>
@foreach (var page in breadcrumbs)
{
@Html.ContentLink(page) <span>/</span>
}
<strong>@currentPage.Name</strong>
</div>
Search-based navigation
You can also retrieve content through searching based on a set of criteria, or through query-based searches. You can use Optimizely Search & Navigation to add more powerful search features. Search & Navigation is automatically included if you run the Digital Experience Platform (DXP).
Updated 7 months ago