One common request for most Sitecore website I’ve worked on is there is a way to maintain a global header and footer item, for the ease of maintenance as well as to prevent unintended variations from being published.

While there are several different ways to do this, the intention of this post is to create a simplified experience that allows for Experience Editor use, but also provides the ability to override specific pages if necessary. This will allow you to make changes to your global and footer in one place, without having to worry about repeating presentation on template standard values or at an item level.

The basic premise behind this is that we’re going to add presentation details to a global item for the header and footer, then tap into the Sitecore pipelines to inject that presentation into each page, along with the resentation details of the item itself.

The Code

To do this, we begin by creating a new processor in the <mvc.getXmlBasedLayoutDefinition> pipeline. We create a config file like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getXmlBasedLayoutDefinition>
        <processor patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.GetXmlBasedLayoutDefinition.GetFromLayoutField, Sitecore.Mvc']"
						   type="MasterToWeb.Feature.PageContent.Pipelines.GetXmlBasedLayoutDefinition.GetFromLayoutField, MasterToWeb.Feature.PageContent"/>
      </mvc.getXmlBasedLayoutDefinition>
    </pipelines>
  </sitecore>
</configuration>

As you can see, we overwrote an existing processor with our own. But don’t worry, we won’t lose any out of the box features.  Now, we want to implement our new processor:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using Sitecore;
using Sitecore.Mvc.Pipelines.Response.GetXmlBasedLayoutDefinition;
using Sitecore.Mvc.Presentation;
using Sitecore.Sites;
using Sitecore.Data.Items;
using Sitecore.Data.Fields;
using Sitecore.Diagnostics;
using System.Web.Caching;
using Sitecore.Mvc.Extensions;

namespace MasterToWeb.Feature.PageContent.Pipelines.GetXmlBasedLayoutDefinition
{
    /// <summary>
    /// The processor replaces the out of the box processor, but retrieves the layout the same way.  
    /// However, it looks to see if the page utilizes a global header/footer, and injects the presentation
    /// from a globally defined setting or an individual page setting into the presentation XML.
    /// </summary>
    public class GetFromLayoutField : GetXmlBasedLayoutDefinitionProcessor
    {
        public override void Process(GetXmlBasedLayoutDefinitionArgs args)
        {
            if (args.Result != null || PageContext.Current.Item == null) return;

            SiteContext siteInfo = SiteContext.Current;
            if (siteInfo == null) return;

            string key = string.Format("LayoutXml.{0}.{1}",
                Context.Language.Name,
                PageContext.Current.Item.ID);

            //TODO: Below commented code would enable caching of the results for performance.  
            //TODO: Currently disabled, but should be validated once the actual header components are added.

            //XElement pageLayoutXml = (XElement)HttpContext.Current.Cache[key];
            //if (pageLayoutXml != null && Context.PageMode.IsNormal)
            //{
            //    args.Result = pageLayoutXml;
            //    return;
            //}

            XElement content = GetFromField(PageContext.Current.Item);
            if (content != null && (Context.PageMode.IsPreview || Context.PageMode.IsNormal))
            {
                Item item = PageContext.Current.Item;

                if (item != null &&
                    (item.DescendsFrom(Constants.Templates.HasGlobalStructure.ID)) &&
                    !item.TemplateID.Equals(Constants.Templates.HeaderPage.ID) &&
                    !item.TemplateID.Equals(Constants.Templates.FooterPage.ID))
                {
                    XElement currentPageDxElement = content.Element("d");
                    if (currentPageDxElement == null)
                    {
                        args.Result = content;
                        return;
                    }

                    //if item uses global page structure, pull settings from root item
                    Item rootItem = Context.Database.GetItem(siteInfo.RootPath);

                    //header
                    Item headerItem = MainUtil.GetBool(item[Constants.Templates.HasGlobalStructure.Fields.UseGlobalHeader], false)
                        ? rootItem
                        : item;

                    if (headerItem != null && headerItem.DescendsFrom(Constants.Templates.GlobalPageStructure.ID))
                    {
                        if (!string.IsNullOrEmpty(headerItem[Constants.Templates.GlobalPageStructure.Fields.HeaderItem]))
                        {
                            currentPageDxElement.Add(GetContent(headerItem[Constants.Templates.GlobalPageStructure.Fields.HeaderItem]));
                        }
                    }


                    //footer
                    Item footerItem = MainUtil.GetBool(item[Constants.Templates.HasGlobalStructure.Fields.UseGlobalFooter], false)
                       ? rootItem
                       : item;
                  
                    if (footerItem != null && footerItem.DescendsFrom(Constants.Templates.GlobalPageStructure.ID))
                    {
                        if (!string.IsNullOrEmpty(footerItem[Constants.Templates.GlobalPageStructure.Fields.FooterItem]))
                        {
                            currentPageDxElement.Add(GetContent(footerItem[Constants.Templates.GlobalPageStructure.Fields.FooterItem]));
                        }
                    }
                }
            }

            try
            {
                if (Context.PageMode.IsPreview || Context.PageMode.IsNormal)
                {
                    HttpContext.Current.Cache.Add(
                    key,
                    content,
                    null,
                    DateTime.Now.AddMinutes(5),
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.AboveNormal,
                    null);
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error in GetFromLayoutField processor", ex, this);
            }

            args.Result = content;
        }
        /// <summary>
        /// From given Item's Layout extract the elements with given placeholder.
        /// </summary>
        /// <param name="fromItemId"></param>
        /// <param name="fromItemPlaceholder"></param>
        /// <returns></returns>
        private List<XElement> GetContent(string fromItemId)
        {
            if (string.IsNullOrEmpty(fromItemId))
            {
                return new List<XElement>();
            }

            XElement inputLayout = GetItemLayout(fromItemId);
            if (inputLayout != null)
            {
                XElement dXElementInInputLayout = inputLayout.Element("d");
                if (dXElementInInputLayout != null)
                {
                    return dXElementInInputLayout.Elements().ToList();
                }
            }

            return new List<XElement>();
        }

        /// <summary>
        /// Returns Layout Field Value for given ItemID
        /// </summary>
        /// <param name="itemId"></param>
        /// <returns></returns>
        private XElement GetItemLayout(string itemId)
        {
            Item currentItem = Context.Database.GetItem(itemId);
            if (currentItem != null)
            {
                return GetFromField(currentItem);
            }
            return null;
        }


        /// <summary>
        /// Gets the xml from the current items layout field
        /// </summary>
        /// <returns></returns>
        private XElement GetFromField(Item item)
        {
            if (item == null)
            {
                return null;
            }

            LayoutField layoutField = new LayoutField(item);
            if (layoutField == null)
            {
                return null;
            }

            string fieldValue = layoutField.Data.InnerXml;
            if (fieldValue.IsWhiteSpaceOrNull())
            {
                return null;
            }

            return XDocument.Parse(fieldValue).Root;
        }        
    }
}

The code above uses Template ID constants as defined below. These IDs are from my example only. If you create your templates without using the provided serialized unicorn files provided in this example, you MUST change these IDs to match your template information. Alternatively, you can modify the above code to use alternate methods of retrieving the information (i.e. Glass Mapper, etc).

using Sitecore.Data;

namespace MasterToWeb.Feature.PageContent
{
    public class Constants
    {
        public struct Templates
        {
            public struct GlobalPageStructure
            {
                public static readonly ID ID = new ID("{509978E3-D69D-4422-A3C1-97EDE9714887}");
                public struct Fields
                {
                    public static readonly ID HeaderItem = new ID("{E92682B6-116A-4E05-8993-C584207CEBD4}");
                    public static readonly ID FooterItem = new ID("{F19387FF-E43C-4563-B7DD-9573237586ED}");
                }
            }

            public struct HasGlobalStructure
            {
                public static readonly ID ID = new ID("{C2BF62A7-D246-4FC1-AB87-9A062C5F8072}");
                public struct Fields
                {
                    public static readonly ID UseGlobalHeader = new ID("{7C004AE3-143A-497E-A1C6-B0E87524D6F6}");
                    public static readonly ID UseGlobalFooter = new ID("{D7166AA5-E099-461F-9925-683F0459C87F}");
                }
            }

            public struct HeaderPage
            {
                public static readonly ID ID = new ID("{E36AD52B-CDEE-41F9-9745-040FCD5D9272}");
            }  
            
            public struct FooterPage
            {
                public static readonly ID ID = new ID("{6EAD5675-F7A9-42E7-97E7-33DA178BD14C}");
            }
        }
    }
}

The Sitecore Items

You’ll need a few items and settings defined in Sitecore as well. How you implement these templates and where you put them is entirely up to your Sitecore architecture. The items shown below and their locations are just for demonstration purposes.

We begin by defining a global header and global footer item. In the example below, these items are stored at
/MasterToWeb/Modules/Page Content/Page Structure/Global Header and Global Footer. These items are based on the /sitecore/templates/Project/Common/Content Types/Header and
/sitecore/templates/Project/Common/Content Types/Footer Templates, respectively.

In turn, respecting Helix principles, those project-layer templates inherit from the base feature templates located at:
/sitecore/templates/Feature/PageContent/_HeaderPage and
/sitecore/templates/Feature/PageContent/_FooterPage. While these templates don’t contain any actual fields, they can be used to set default prentation information using the standard values.

Figure A

In our example, we then add two fields to the Site Root item (/sitecore/content/MasterToWeb) by having the Site Room item template inherit from our new _Global Page Structure template located at: /sitecore/templates/Feature/PageContent/_Global Page Structure.

This adds the following fields to our Site Root item:

Figure B

In addition, for all content pages (i.e. Home item and child content pages), we add the following base templates to the templates of those items:
_Has Global Structure and _Global Page Structure. By doing this, in addition to the two fields above, we also get these fields at every page level:

Figure C

This is all you need to get started. The logic works as such:

Whenver an mvc rendering is requested, the pipelines checks the item to see if the “Use Global Header” and “Use Global Footer” items are checked on the context item. If they are, it will look at the Site Root item (Figure B) and get the global header and footer items. It will then look at the presentation defined on those items…

…and inject them into the presentation defined on the actual context item.

The end result is a central item that can be used to define the global footer and item. It can even be edited using the Experience Editor by going to the Global Header item and opening it in Experience Editor. This also has the added benefit of improving the perfomance of the Experience Editor in content pages, because the global header and footer will not be loaded while editing a regular content item.

So, what happens if you want to change the header or footer on an individual page to be different than the global versions?

Well, lucky for you, this is already supported. On the content pages, if you uncheck the “Use Global Header” and “Use Global Footer”, then you have the option of defining an ALTERNATE version, which can have it’s own, completley different, presentation.

You can also uncheck the fields, and not select a new header item, and define the presentation at the item level, making this extremely flexible.

The example code, with serialized unicorn files can be found here:
https://github.com/MasterToWeb/Sitecore.GlobalHeaderFooter

Future Updates

  • Currently working on adding the ability to inherit from an “alternate” global item. For example if you want all descendants of item home/page A/... to inherit from an different header defined on item /home/Page A