Shopify Breadcrumbs based on Menu Structure

Please note: The code in this article is obsolete.

The code in this article depended on a product belonging to a top-level menu item. That was a quick and dirty way to get to launch, but I quickly updated it.

This article also lays out a lot of the “why” and that part is still valid, but the newer article has better code. See:

Shopify Breadcrumbs Based on Menu Structure (Improved Version)

Shopify breadcrumbs can be a hassle if you want to have some sort of parent-child relationship between collections and you want to reflect that in your breadcrumbs. Shopify has no inherent concept of hierarchy. Some people impose it by doing things like using automatic collections based on Product Type for the top level, manual collections for the second level and tags for the third level. I’ve been guilty of that. But there are many possible fail points, it has poor abstraction (see also coupling), and it ignores the one place in Shopify where hierarchy is easy to create: the menu system.

Why base breadcrumbs off menus in Shopify?

  1. Good Customer Experience. There’s the obvious reason: it’s the one hierarchy that is exposed to the user on any site.
  2. Shopkeeper Configurable. In Shopify, though there is a, the taxonomy system (collections and tags), the menu system is the only one that allows you to create parent-child relationships.
  3. Nothing Hidden. The shopkeeper has nothing to remember. With a system based on automatic collections, manual collections and tags, you’re asking someone who is busy running a business, not editing their Shopify store every day, to remember how all these pieces are glued together.
  4. Automatically Adapts. If the shopkeeper makes an edit to the menu structure, it is immediately reflected in the breadcrumbs.
  5. Avoids Some Race Conditions. Let’s say a shopkeeper has Left Blue Widget in a hierarchy of collections like this: Widgets → Blue Widgets → Left Blue Widgets. But also, it’s in the Featured collection and the On Sale collection and so on. Using a method based on product type, collections and tags, I get the first breadcrumb element just fine — I just make sure collection title is the same as product type. There can only be one product type per product, so I’m safe. But when I go to get the second-level breadcrumb, I have no way to choose between Blue Widgets, Left Blue Widgets, Featured, On Sale, and The Collection the Shopkeeper Built to Annoy His Developer. And I have the same problem with tags at the third level. Using the menu system, it’s simple. I know which top-level menu item maps to a collection to which the product belongs. When I look for the children, I only look in the child menu that belongs to that top-level term and I look for that collection for my second-level breadcrumb. However, if the item is linked multiple places in the main nav, then of course you still have a race condition, but there’s no system that can really address that without tracking the user through their journey to get to that page and adapting breadcrumbs based on that (some people argue that systems like that are the only true breadcrumbs).

Problems with this system

If you’re a developer reading #5 above, you probably spotted two problems right off the bat.

  1. The coupling problem again. If the product doesn’t belong to a collection that is linked by the top-level menu, you have no breadcrumb. This is the one part of the system that requires some maintenance and violates my desire for loose coupling. My solution is simple: top-level collections are automatic collections based on product type. I wish it were otherwise, but couldn’t see a way around this. So a shopkeeper could create a new product type, fail to map it to an automatic collection and add the link at the top level and, oops!, your breadcrumbs are gone. If anyone knows a way around this, I’d love to hear it.
  2. The race condition problem again. If the shopkeeper adds the product to two collections, both of which are linked in the same menu (i.e. main menu or the same child menu), then you have a race condition and the breadcrumb will be based on the last matching collection it finds, not necessarily the one the customer clicked. Again, there’s no way around this. If the shopkeeper has Widgets and On Sale as top-level menu items, the breadcrumb will be based on the first matching collection. In that case, you may need to blacklist some collections from your breadcrumb.

Why doesn’t everyone do Shopify breadcrumbs this way?

I haven’t seen anyone propose this way to do breadcrumbs. I don’t believe it’s because of the problems with the system mentioned above. I think it’s quite simply a lack of vision. It’s so obvious once you see it, but it didn’t occur to me until I saw the Helium Dev post on creating sub-collections (which is essentially what we’re doing here). Once I saw that I thought “Of course!” and saw many possible applications, breadcrumbs being just one.

So how do I create breadcrumbs based on menu structure in Shopify?

Good God Tom, does everything take a thousand words of setup before getting to the point? Yes, actually, it does. For me. It’s one of my endearing qualities. Or not.

This gets pretty messy and once you see how much code is needed to get a simple breadcrumb, you might ask why. Now you either know, or have fallen asleep. Either way, problem solved.

At a high level the process looks like this:

  • Get an array of links to all the collections the product in question belongs to. We need this to test against with contains.
  • Iterate through the Main Menu and find the top-level link that is contained in the array of links we just created.
  • Find out if that top-level item has a child linklist (aka dropdown menu) and get its handle.
  • Repeat the process for the submenus.

Because Liquid is just a templating language and not a proper programming language, it doesn’t allow a lot of things: direct assignment of array elements, associative arrays, functions, recursion and so on. So the other big limitation here is that you only drill down as many levels as you hardcode. No recursing out branch after branch until you hit a leaf.

Create an array of links to the product collections.

Again, first we’re going to be able to quickly test to see if a given URL points to a collection that the product belongs to. So we grab all those URLs and put them in an array.

{%- capture product_collection_url_string -%}{%- for collection in product.collections -%}{{collection.url }}|{%- endfor -%}{%- endcapture -%}
 {%- assign str_size = product_collection_url_string | size | minus: 1 -%}
 {%- assign product_collection_url_string = product_collection_url_string | slice: 0, str_size -%}
 {%- assign product_collection_urls = product_collection_url_string | split: '|' -%}
By the way, are you wondering why that code above looks so, ahem, pretty? It’s using my syntax highlighting for Liquid plugin!

Compare product collection links to main menu links

Now we just iterate through our top-level menu and find out which is the highest level category this item belongs to.

We do have a minor difficulty here though. The collection handle is not the same as the linklist handle. However, Shopify has a strict requirement for creating dropdowns. The title of the submenu must be identical to the title text of the parent link. So we can iterate through the linklists and when the linklist title matches the link title, we know we’ve found the child linklist. So then we simply grab the handle of that linklist and save it for the next step. Then we create our first breadcrumb piece, bc1.

    {%- for link in linklists.main-menu.links -%}
      {%- if product_collection_urls contains link.url and link.type == "collection_link" -%}
        {%- for childlist in linklists -%}
          {%- if childlist.title == link.title -%}
                {%- assign bc1_collection_handle = link.url | split: '/' | last -%}
                {%- assign bc1_linklist_handle = childlist.handle -%}
          {%- endif -%}
        {%- endfor -%}
        {%- capture bc1 -%}{{ link.title | link_to: link.url, link.title }}{%- endcapture -%}
      {%- endif -%}
    {%- endfor -%}

Grab the child linklist

Assuming we have a linklist handle from the previous step, we now need to create an array of URLs that appear in that child menu. We’ll test against that array as we do against the array of product collection URLs. It’s a bit aggravating that we can’t create a function, but Shopify Liquid language only lets us pass in one variable with the with parameter. Alternatively, we can of course assign variables that would be available to variable with the same name within the snippet, include the snippet and then assign the values to the variables we need, but this ends up taking almost as much code as doing it on the fly each time.

    {%- unless bc1_linklist_handle == blank -%}
      {%- capture child_url_string -%}{%- for link in linklists[bc1_linklist_handle].links -%}{{ link.url }}|{%- endfor -%}{%- endcapture -%}
      {%- assign str_size = child_url_string | size | minus: 1 -%}
      {%- assign child_url_string = child_url_string | slice: 0, str_size -%}
      {%- assign child_urls = child_url_string | split: '|' -%}

      {%- for link in linklists[bc1_linklist_handle].links -%}
        {%- if child_urls contains link.url and product_collection_urls contains link.url and link.type == "collection_link" -%}
          {%- for childlist in linklists -%}
            {%- if childlist.title == link.title -%}
              {%- assign bc2_collection_handle = link.url | split: '/' | last -%}
              {%- assign bc2_linklist_handle = childlist.handle -%}
            {%- endif -%}
          {%- endfor -%}
          {%- capture bc2 -%}{{ link.title | link_to: link.url, link.title }}{%- endcapture -%}
        {%- endif -%}
      {%- endfor -%}
    {%- endunless -%}

Grab more breadcrumbs

At this point, you just repeat the above process if you want to go down more levels (and at a certain point it will become worth it to have a snippet that you include each time). The only difference is that when you get to your last level, you don’t need to grab childlist.handle because you’re not going any deeper anyway.

    {%- unless bc2_linklist_handle == blank -%}
      {%- capture child_url_string -%}{%- for link in linklists[bc2_linklist_handle].links -%}{{ link.url }}|{%- endfor -%}{%- endcapture -%}
      {%- assign str_size = child_url_string | size | minus: 1 -%}
      {%- assign child_url_string = child_url_string | slice: 0, str_size -%}
      {%- assign child_urls = child_url_string | split: '|' -%}

      {%- for link in linklists[bc2_linklist_handle].links -%}
        {%- if child_urls contains link.url and product_collection_urls contains link.url and link.type == "collection_link" -%}
          {%- capture bc3 -%}{{ link.title | link_to: link.url, link.title }}{%- endcapture -%}
        {%- endif -%}
      {%- endfor -%}
    {% endunless %}

Display your breadcrumb

Now we simply take the two or three breadcrumb pieces we’ve created and output them as links. This is based on the breadcrumb styling in the Timber framework for Shopify.

    {%- if bc1 -%}
      <span aria-hidden="true">›</span>
      {{ bc1 }}
    {%- endif -%}
    {%- if bc2 -%}
      <span aria-hidden="true">›</span>
      {{ bc2 }}
    {%- endif -%}
    {%- if bc3 -%}
      <span aria-hidden="true">›</span>
      {{ bc3 }}
    {%- endif -%}
    <span aria-hidden="true">›</span>
    <span>{{ product.title }}</span>

Wrapping up loose ends

So that takes care of products and fires whenever we’re using a product template, as in whenever this is true:

{%- if template contains 'product' -%} 

We can use the same method for collections. Pages present a problem unto themselves, since they don’t belong to collections. Your best bet there is probably to simply make sure the page itself links back to the relevant products if needed. In any case, I haven’t found a great solution there.

4 thoughts on “Shopify Breadcrumbs based on Menu Structure”

  1. Hi Tolam Bert, can you help me with my website, I am in Vietnam using a service like Shopify, you are using Google translate to talk to you. Please feel free to contact me by email and thank you for any help

  2. Hey Tom; this guide seems to be almost exactly what I’m looking for. What are the chances of this being updated to reflect the nested menu changes that Shopify implemented? I reckon you’d make quite a few searchers very happy! 🙂

    • Actually I also need to update it for another reason. As described it requires strict ancestor/descendant chains building a tree from top to bottom. I generalize it more for the final implementation so that it makes far fewer assumptions about term hierarchy and such. I just never went back and updated it. But I was thinking yesterday that I should do so. Gets VERY little search volume though so I don’t think it would make all that many people happy.

      What’s your timeline?


Leave a Comment