loading...
Cover image for Breadcrumb created from URL for Craft CMS

Breadcrumb created from URL for Craft CMS

piotrpog profile image Piotr Pogorzelski Originally published at craftsnippets.com ・4 min read

This article was originally posted on craftsnippets.com, blog about Craft CMS. Head there for more Craft-related articles and ready to use template components.


Component overview

This template component can be dropped into any project - it works out of the box, without any modifications. Here are it's features summed up:

  • Component is styled using bulma classes.
  • It has google structured data attributes to improve your SEO.
  • It also has proper ARIA attributes. ARIA stands for Accessible Rich Internet Applications - it's standard which seeks to help people with disabilities navigate websites using tools like screen readers.
  • Breadcrumb will only show up if multiple links exist, since breadcrumb with just one link is not really useful.

Breadcrumb component itself:

{# v3 #}
{# http://craftsnippets.com/articles/breadcrumb-created-from-url-for-craft-cms #}

{# settings #}
{% set nonElementLinks = false %}

{# populate breadcrumbLinks array if no array of links was provided #}
{% if breadcrumbLinks is not defined %}
    {% set breadcrumbLinks = [] %}
    {# home #}
    {% set home = craft.app.getElements().getElementByUri('__home__', currentSite.id) %}
    {% set breadcrumbLinks = breadcrumbLinks|merge([{
        url: home.url ?? alias(currentSite.baseUrl),
        title: home.title ?? 'homepage'|t,
    }]) %}

    {# get elements #}
    {% set segments = craft.app.request.segments %}
    {% for segment in segments %}
            {% set uriPart = segments[0:loop.index]|join('/')|literal %}
            {% set element = craft.app.elements.getElementByUri(uriPart, currentSite.id) %}
            {% if element %}
                {% set breadcrumbLinks = breadcrumbLinks|merge([{
                    url: element.url,
                    title: element.title,
                }]) %}
            {% elseif nonElementLinks %}
                {% set breadcrumbLinks = breadcrumbLinks|merge([{
                    url: url(uriPart),
                    title: segment|t,
                }]) %}
            {% endif %}
    {% endfor %}
{% endif %}

{# render breadcrumb #}
{% if breadcrumbLinks|length > 1 %}
<nav class="breadcrumb" aria-label="{{'breadcrumbs'|t}}">
    <ul itemscope itemtype="http://schema.org/BreadcrumbList">
        {% for link in breadcrumbLinks %}
            <li class="{{loop.last ? 'is-active'}}" {{loop.last ? 'aria-current="page"' }} itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
                <a href="{{link.url}}" itemtype="http://schema.org/Thing" itemprop="item">
                    <span itemprop="name">{{ link.title }}</span>
                </a>
            </li>
        {% endfor %}
    </ul>
</nav>
{% endif %}

How does it work?

For each URL segment, element query using getElementByUri method is performed. This query uses URI parameter that is composed of this segment and all segments preceding it. If the query is successful, title of the returned object is used as specific breadcrumb link text value. getElementByUri will go through all elements that have URI attribute - entries, categories and even custom ones added by plugins.

First breadcrumb link is not represented by URL segment - it is homepage link. It is associated with entry with empty URI, queried by special slug __home__. title of that entry will be used as text value of link. If no such entry exists, string homepage passed through translation filter will be used instead.

Custom route links

If getElementByUri method returns nothing, it means that URL segment is not associated with entry, category or any other custom element page. Most likely it represents some custom URL route. In such a situation, you can do one of these things:

  • Set up an array of links yourself and pass it manually to the component as breadcrumbLinks - like this: {% include 'breadcrumb' with {breadcrumbLinks: breadcrumbLinks} %}. If component receives such array, it will not generate links itself but use these passed to it.
  • Set variable nonElementLinks at begining of the component to true. This will cause links not associated with elements pages to be appended to the breadcrumb. Their text values will be taken from URL segment and passed trough |t filter so you can set them up in static translations.

Breadcrumb with Seomatic

If you are already using Seomatic on your website, you can use it's built-in functionality to get data required to render links. Here's breadcrumb component adjusted for Seomatic:

{% set breadcrumbLinks = seomatic.jsonLd.get('breadcrumbList').itemListElement %}
{% if breadcrumbLinks|length > 1 %}
<nav class="breadcrumb" aria-label="{{'breadcrumbs'|t}}">
    <ul>
        {% for link in breadcrumbLinks %}
            <li class="{{loop.last ? 'is-active'}}" {{loop.last ? 'aria-current="page"'}}>
                <a href="{{ link.item['@id'] }}">
                    <span itemprop="name">{{ link.item['name'] }}</span>
                </a>
            </li>
        {% endfor %}
    </ul>
</nav>
{% endif %}

Note that this version of breadcrumb is stripped of it's structured data attributes - Seomatic renders proper JSON-LD code for breadcrumb which does the same job.

Seomatic breadcrumb also doesn't include links not related to element pages. To learn how to add them to breadcrumb, head to Seomatic docs and look for "To entirely replace the existing BreadcrumbList on a page".

Why is such breacrumb component useful?

Craft CMS allows us to build websites with URLs consisting of multiple segments. If your website has many types of categories and sections, your URL logic can get quite complicated. For example, let's consider such URL for article page:

category-list/1-level-category/2-level-category/article-page

Here are URL settings of various sections and categories needed to achieve such URL structure:

  • Article page section needs to have categoryField category field and such url setings: {categoryField.last.uri}/{slug}.
  • Categories have multiple levels, so for category group, URL settings would be: {parent.uri ?? craft.entries.section('category_list').one.uri}/{slug}.
  • Category list is single type section, so it only needs hardcoded entry title: category-list.

As you can see, these URL settings use element queries and conditional operators. We could reproduce all this logic within breadcrumb component in Twig. But we don't need to. Remember - Don't Repeat Yourself.

Breadcrumb plugin

As an alternative to breadcrumb template component described in this article, you can use Breadcrumb plugin. It works pretty much the same while featuring few useful configuration options, like:

  • customising title of first breadcrumb link directing to homepage.
  • Setting maximum amount of breadcrumb links
  • Skipping specific segments

Just like with all plugins - before installing it consider if you really need it. Keep in mind that each plugin adds additional level of complexity to your website.

Article update history

  • 1 july 2019 - breadcrumb homepage link is now created from homepage url or currentSite.baseUrl passed through alias function - to take into account situation when site base url is set to @web.

Discussion

markdown guide