Better breadcrumb trail

Posted on November 14, 2009 by Frederik Vig in EPiServer, Web design

I often install the Public Templates when setting up a new EPiServer project. They contain some good code that I reuse in various parts of a new site. One thing that I’ve copied and modified is the code for the breadcrumb (breadcrumb.ascx). By default the markup rendered looks something like this.

<a href="/en/" title="To start page">Start</a> / 
<a href="/en/Events/" title="Events">Events</a> / 
<a href="/en/Events/Conference/" title="Conference">Conference</a>

There are a number of problems with this code. First of the markup is not very semantic. Second the title attribute is redundant, since the same text gets repeated twice, and thus read by screen readers twice. Third the page we’re currently on should not be a link, since we’re already on it. It should also have some help text for screen reader users giving information that “Conference” in fact is the current page.

Better markup

Okay lets fix the semantics first. Lets put the links inside a list and remove the forward slash (/), which is presentational, something that should be handled by CSS.

<ul>
<li><a href="/en/" title="To start page">Start</a></li>
<li><a href="/en/Events/" title="Events">Events</a></li>
<li><a href="/en/Events/Conference/" title="Conference">Conference</a></li>
</ul>

This is better, but we’re not quite there yet. An unordered list is not the correct list type here. The meaning of the list changes when we change the order of the links, we should therefor use an ordered list instead. Lets update the list to be ordered, and lets also get rid of all the title attributes.

<ol>
<li><a href="/en/">Start</a></li>
<li><a href="/en/Events/">Events</a></li>
<li><a href="/en/Events/Conference/">Conference</a></li>
</ol>

That’s better! Now we only have the “Conference” link left. Lets remove the anchor, and add some helper text for our screen reader users.

...
<li>Conference <span>(this page)</span></li>
...

A little styling

Great, now with a little CSS we can transform this to a nice, user friendly breadcrumb trail.

#breadcrumb {
    list-style: none;
}
 
#breadcrumb li {
    display: inline;
    margin: 0;
    padding: 0;
}
 
#breadcrumb a
{
    color: #3e3e3e;
	float: left;
	margin-right: .5em;
	padding-right: 1em;
	background: url(images/separator.gif) no-repeat right center;
}
 
#breadcrumb span {
    position: absolute;
    left: -9999px;
}

Nothing particular here, instead of adding extra markup for the separator, we’re using a background image. And for the helper text we’re using a technique for pushing the span element all the way to the left (off screen), hiding it from all except screen reader users. Why not use display: none instead? Well because some screen readers don’t read text that has the display property set to none.

The result


Nothing exciting here, but we now have a much more semantic breadcrumb, that is very easy to style to our needs.

Implementation in EPiServer

This is code based on the Public Templates breadcrumb.ascx, modified for our purpose.

using System;
using EPiServer.Core;
using System.Text;
 
namespace EPiServer.Templates.Public.Units.Static
{
    public partial class BreadCrumbs : UserControlBase
    {
        private const string _link = "<li><a href=\"{0}\">{1}</a></li>";
        private int _maxLength = 60;
        private string _breadcrumbId = "breadcrumb";
 
        protected override void OnPreRender(EventArgs e)
        {
            Breadcrumbs.Text = GenerateBreadCrumbs(CurrentPage);
        }
 
        private string GenerateBreadCrumbs(PageData page)
        {
            // Initiate a string builder based on max length. The generated html is considerably longer than the visible text.
            var breadCrumbsText = new StringBuilder(8 * MaxLength);
 
            // Initiate a counter holding the visible length of the bread crumbs with the length of the start page link text.
            int breadCrumbsLength = Translate("/navigation/startpage").Length;
 
            while (page != null && !page.PageLink.CompareToIgnoreWorkID(PageReference.StartPage))
            {
                breadCrumbsLength += page.PageName.Length;
                if (breadCrumbsLength > MaxLength)
                {
                    breadCrumbsText.Insert(0, "...");
                    break;
                }
 
                // Insert the link at beginning of the bread crumbs string 
                breadCrumbsText.Insert(0, this.GetLink(page));
 
                // Get next visible parent
                page = this.GetParentPageData(page);
            }
 
            // Generate the start page link 
            string startPageLinkUrl = string.Empty;
            if (string.IsNullOrEmpty(startPageLinkUrl))
            {
                startPageLinkUrl = Server.UrlPathEncode(GetPage(PageReference.StartPage).LinkURL);
            }
 
            string startPageLink = string.Format(_link, startPageLinkUrl, Translate("/navigation/startpage"));
            breadCrumbsText.Insert(0, startPageLink);
 
            string listStart = string.Format("<ol id=\"{0}\"", _breadcrumbId);
 
            breadCrumbsText.Insert(0, listStart);
            breadCrumbsText.Append("</ol>");
            return breadCrumbsText.ToString();
        }
 
        /// <summary>
        /// Get the next visible parent page of the supplied <see cref="PageData"/>. 
        /// </summary>
        /// <param name="page"></param>
        /// <returns>The <see cref="PageData"/> object or    </returns>
        private PageData GetParentPageData(PageData pageData)
        {
            // Don't return a PageData object for start page or root page.
            if (pageData == null || pageData.ParentLink == PageReference.StartPage || pageData.ParentLink == PageReference.RootPage)
            {
                return null;
            }
 
            // Get Page data for parent page
            pageData = GetPage(pageData.ParentLink);
            if (pageData != null && pageData.VisibleInMenu)
            {
                return pageData;
            }
            // Step up to next parent
            return GetParentPageData(pageData);
        }
 
        /// <summary>
        /// Returns a anchor based on a <see cref="PageData"/> object.
        /// </summary>
        private string GetLink(PageData page)
        {
            string pageName = page.Property["PageName"].ToWebString();
 
            if (page.PageLink.CompareToIgnoreWorkID(CurrentPage.PageLink))
            {
                return string.Format("<li>{0} <span>(this page)</span></li>", pageName);
            }
 
            return string.Format(_link, Server.UrlPathEncode(page.LinkURL), pageName);
        }
 
        /// <summary>
        /// Sets the max length on the visible breadcrumb text (default = 60)
        /// </summary>
        public int MaxLength
        {
            get { return _maxLength; }
            set { _maxLength = value; }
        }
 
        public string BreadcrumbId
        {
            get { return _breadcrumbId; }
            set { _breadcrumbId = value; }
        }
    }
}

One thing to note here is that I’ve hard coded the text (this page). This should be placed in a language file instead.

Other resources

Related Posts: