Personalize a site header in Sitecore

In this post, I’ll be describing one option for personalizing the header in Sitecore. This can be a challenging requirement. Most of the time, header renderings are added to a base page template and look specifically at a specific path in the Sitecore content tree that has items defined for the navigation menus. When this is the case, you lose the ability to personalize the menu.

There is another problem with this though. Normally, personalizing like this requires that the items be edited through the experience editor and that datasources back these items. Additionally, since the presentation details of the page are stored at the page level, we would have to make sure that the header had it’s datasource set on every page.

The approach below was created to augment an already created site that already set headers and footers on a base page. The header and footers get their data via a controller rendering. So in order to not create an authoring nightmare, we modified the header and footer controller. This approach allows the content author to edit the header and menu items via the experience editor and set personalization rules for rendering just as if they renderings on a page.

An example. In the images below, the user clicks on “Main Link 5” and a panel appears with navigation links. To the right of the links there is a small callout that can be personalized.

Personalized for roadrunner hunters
Personalized for rabbit hunters

How this works

The header template and rendering

Start by creating a blank layout for your header template. The layout needs to support 2 modes, editing in the experience editor and normal site rendering. Next, create your header template. Assign the blank layout to the header’s presentation details. Here is an example of a blank layout.

@using Glass.Mapper.Sc.Web.Mvc
@using Sitecore.Mvc

<!-- Sitecore.Context.PageMode.IsExperienceEditorEditing: @Sitecore.Context.PageMode.IsExperienceEditorEditing -->
<!-- Sitecore.Context.PageMode.IsPreview: @Sitecore.Context.PageMode.IsPreview -->

@if (Html.Glass().IsInEditingMode)
{
    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <link rel="stylesheet" href="~/Include/styles.css" />
    </head>
    <body>
        @Html.Sitecore().Placeholder("header-empty-layout")
    </body>
</html>
}
else
{
@Html.Sitecore().Placeholder("header-empty-layout")
}

The header template has a multi-list field called “Menus” that allows you choose menus and arrange the order of the menus.

The menu template

The menu template is a simple template that also has a layout on it. Other renderings have been created and using dynamic placeholders, we are able to construct a menu panel as you would a normal page in Sitecore via the experience editor. Hear is an example of the menu template. The layout is set on the template itself. The image below shows the actual menu item with presentation details set via the experience editor.

We have a “Site Settings” template, where the header template and footer template are assigned. This is shown here.

The header is a controller rendering. The controller rendering looks at the “Default” method in the following code. The code looks at the assigned Header template in the Site Settings and foreach menu selected, passes it through the “Presentation Inversion Service”

using System;
using System.Text;
using System.Web.Mvc;
using Glass.Mapper.Sc.Web.Mvc;
using Sitecore.Data;

namespace SampleSite.Header
{
    public class HeaderController : GlassController<IHeader>
    {
        private PresentationInversionService _presInvSrv;

        public HeaderController()
        {
            _presInvSrv = new PresentationInversionService();
        }

        public ActionResult Default()
        {
            var model = new HeaderViewModel(Context);

            if (Context.Menus != null)
            {
                var sbDesktop = new StringBuilder();

                foreach (var menu in Context.Menus)
                {
                    sbDesktop.Append(_presInvSrv.GetHtml(menu, System.Web.HttpContext.Current.Request));
                }

                model.DesktopMenus = sbDesktop.ToString();
            }

            return View("/Views/Feature/Navigation/Header/Header.cshtml", model);
        }
    }
}

Using Sitecore’s rendering pipeline

The presentation inversion service is below. Essentially, what allows this to work and personalize the content of the header, is that we are passing the items through the Sitecore rendering pipeline. Even though these items aren’t pages, it still works. The items have layouts and the rendering pipeline will render these items just like they it would for any other page. One other important thing to note is that we are taking the html output of the rendering pipeline and adding that to page directly. In the code above, we call “GetHtml” passing in the menu and the request context.

using System;
using System.Web;
using Sitecore.Data;
using Sitecore.Mvc.Common;
using Sitecore.Mvc.Pipelines;
using Sitecore.Mvc.Pipelines.Response.GetPageRendering;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;
using Sitecore.Sites;
using PageContext = Sitecore.Mvc.Presentation.PageContext;

namespace SampleSite.Header
{
    public class PresentationInversionService
    {
        public string GetHtml(IGlassBase item, HttpRequest request)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }

            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            var scItem = Sitecore.Context.Database.GetItem(ID.Parse(item.Id));

            var pageContext = new PageContext
            {
                RequestContext = request.RequestContext,
                Item = scItem
            };

            using (PlaceholderContext.Enter(new PlaceholderContext("/")))
            {
                using (ContextService.Get().Push<PageContext>(pageContext))
                {
                    var currentContextItem = Sitecore.Context.Item;

                    Sitecore.Context.Item = scItem;

                    using (var writer = new System.IO.StringWriter())
                    {
                        var pageDef = pageContext.PageDefinition;

                        var getPageRenderingArgs = new GetPageRenderingArgs(pageDef);

                        PipelineService.Get().RunPipeline("mvc.getPageRendering", getPageRenderingArgs);

                        var rendering = getPageRenderingArgs.Result;

                        var renderRenderingArgs = new RenderRenderingArgs(rendering, writer);

                        var requireRevertMode = false;

                        if (Sitecore.Context.PageMode.IsExperienceEditor)
                        {
                            Sitecore.Context.Site.SetDisplayMode(DisplayMode.Normal, DisplayModeDuration.Remember);
                            requireRevertMode = true;
                        }

                        PipelineService.Get().RunPipeline("mvc.renderRendering", renderRenderingArgs);

                        if (requireRevertMode)
                        {
                            Sitecore.Context.Site.SetDisplayMode(DisplayMode.Edit, DisplayModeDuration.Remember);
                        }

                        Sitecore.Context.Item = currentContextItem;

                        return writer.ToString();
                    }
                }
            }
        }
    }
}

Here is an example of what this looks like in the Experience Editor. This is a menu item that is being edited. Below is an example of the items that are used as datasources for this specific menu rendering.

About Phil Paris

Hi, my name is Phil Paris and I’m a Sitecore Architect and general Sitecore enthusiast. I’ve been working with Sitecore since 2013. Through this blog I will be sharing Sitecore insights, tips and tricks, best practices and general knowledge with the hopes to further the community at large. Please feel free to reach out to me at any time!

View all posts by Phil Paris →