Link collections and navigation
Introduces options for building content navigation in Optimizely Content Management System (CMS).
You can 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. Because 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
are 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 and FullRefreshPropertiesMetaData. We strongly recommend that you learn how to use DisplayTemplates and UIHint 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 named 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 the preview is 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;
}
...
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 include adding a user-defined property to the site's start page or having a special content type just for site settings. You then load this content item 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 built from the children of the start page. Breadcrumbs renders a list of 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 children of a specific page or through a search. You can use some types to let editors construct an 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 user-defined and built-in values.
Display in navigation
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 template build; it is up to you as a developer to implement this functionality. There are a few examples below of how this can be done.
Create a page list
To illustrate listings, create 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 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.
You 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 child pages below the current page and create a view model.
public class ArticlePageController: PageController<ArticlePage> {
private readonly IContentLoader _contentLoader;
public ArticlePageController(IContentLoader contentLoader) {
_contentLoader = contentLoader;
}
public ActionResult Index(ArticlePage currentPage) {
// Load all article pages that are direct children to the current page.
var pages = _contentLoader.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);
}
}
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 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 the location of list
You can load other pages than the children of the current page. The following example lets an editor pick the parent page for the list of pages by adding a property to your 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) {
// Load all article pages that are direct children to the page specified by
// the ParentForList property.
var pages = _contentLoader.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 funtionality 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) {
// 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 = _contentLoader.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. See Search and filter.
Page lists 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 pages on the site because 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: first, 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 visitors 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 the first item followed by children directly below the start page.
@{
HelperResult TopMenuTemplate(HtmlHelperExtensions.MenuItem menuItem)
{
<li>
@if (menuItem.Selected) {
<strong>@Html.ContentLink(menuItem.Page)</strong>
}
else {
@Html.ContentLink(menuItem.Page)
}
</li>
return new HelperResult(w => Task.CompletedTask);
}
}
<nav>
<ul>
<li>
@Html.ContentLink(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.
@{
HelperResult SubMenuTemplate(HtmlHelperExtensions.MenuItem menuItem)
{
<li>
@if (menuItem.Selected)
{
<strong>@Html.ContentLink(menuItem.Page)</strong>
<ul>
@Html.MenuList(menuItem.Page.ContentLink, SubMenuTemplate)
</ul>
}
else
{
@Html.ContentLink(menuItem.Page)
}
</li>
return new HelperResult(w => Task.CompletedTask);
}
}
<nav>
<ul>
@Html.MenuList(ContentReference.StartPage, SubMenuTemplate)
</ul>
</nav>
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.Web.Routing
@inject IContentLoader loader
@inject IContentRouteHelper contentRouteHelper
@{
var currentPage = contentRouteHelper.Content;
var filter = new FilterContentForVisitor();
var breadcrumbs = loader.GetAncestors(currentPage.ContentLink)
.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 Optimizely Digital Experience Platform (DXP).
Updated 4 months ago