DEV Community

Eiji Saito
Eiji Saito

Posted on

How to Add Breadcrumb Navigation to Shopify Using Theme App Extensions — A Complete Guide

Table of Contents


Introduction

Breadcrumb navigation is one of those small UI elements that punches well above its weight. For users, breadcrumbs provide a clear sense of location within a store and a quick way to jump back to parent categories. For search engines, breadcrumbs provide structured hierarchy signals that improve how your pages appear in search results — Google can display breadcrumb trails directly in SERPs when it detects proper markup.

Despite this, many Shopify themes either lack breadcrumbs entirely or offer limited, hard-coded implementations that merchants can't customize. Building breadcrumbs as a Theme App Extension solves this perfectly: merchants get a configurable breadcrumb block they can place anywhere in the theme editor, and your code stays completely isolated from the theme's source files.

In this tutorial, I'll walk you through building a fully customizable breadcrumb navigation app block for Shopify. We'll handle every major page type — products, collections, articles, blogs, and static pages — and output JSON-LD structured data so search engines can parse the breadcrumb trail. By the end, you'll have production-ready code that merchants can configure from the theme editor with zero coding.


What We'll Build

Here's the finished breadcrumb navigation on a product page:

breadcrumb navigation

Merchants configure the delimiter, colors, font size, and mobile visibility directly from the theme editor:

the theme editor

The breadcrumb block will:

  • Automatically generate the correct breadcrumb trail for products, collections, articles, blogs, and pages
  • Output JSON-LD structured data for SEO
  • Let merchants customize the delimiter, font size, text color, link color, and home link text
  • Support toggling visibility on mobile and desktop independently
  • Work across all Online Store 2.0 themes

What is a Theme App Extension?

Theme App Extensions let Shopify app developers inject UI components — called app blocks — into a merchant's theme without touching the theme's source code.

Why this matters for breadcrumbs:

  • No theme code editing — Merchants add the breadcrumb block via the theme editor, just like any native section or block.
  • Theme-safe — Your code lives inside your app. Theme updates won't overwrite or break your breadcrumbs.
  • Uninstall-clean — When the app is uninstalled, the breadcrumb block is automatically removed. No leftover Liquid snippets.

App blocks are built with Liquid, CSS, and JavaScript — the same stack used in Shopify themes. If you've customized a Shopify theme before, you already know the tools.

For more details, see the official Shopify documentation on Theme App Extensions.


Creating the Extension

Prerequisites

Make sure you have:

Generate the Extension

Inside your app directory, run:

shopify app generate extension --template theme_app_extension --name smart-breadcrumbs
Enter fullscreen mode Exit fullscreen mode

This creates the following structure:

extensions/
  smart-breadcrumbs/
    blocks/
    assets/
    locales/
    snippets/
Enter fullscreen mode Exit fullscreen mode

Directory Overview

Directory Purpose
blocks/ Liquid files that become app blocks in the theme editor
assets/ CSS and JavaScript files
locales/ Translation files for multi-language support
snippets/ Reusable Liquid snippets

We'll create three files:

  1. blocks/breadcrumbs.liquid — the main Liquid template with breadcrumb logic and schema
  2. assets/breadcrumbs.css — styles for the breadcrumb component

Let's start building.


Building the Breadcrumb Markup

Create the app block file at extensions/smart-breadcrumbs/blocks/breadcrumbs.liquid.

The core challenge with breadcrumbs on Shopify is that different page types require different breadcrumb structures. A product page might show Home > Collection > Product, while a blog article shows Home > Blog > Article. We need to detect the current page type using the template object and build the trail accordingly.

The Breadcrumb Container

First, let's set up the outer wrapper and the Home link, which is common to every page type:

{{ 'breadcrumbs.css' | asset_url | stylesheet_tag }}

{% assign delimiter = block.settings.delimiter %}
{% assign home_text = block.settings.home_text %}

<nav
  class="app-breadcrumbs"
  aria-label="Breadcrumb"
  style="
    --breadcrumb-font-size: {{ block.settings.font_size }}px;
    --breadcrumb-text-color: {{ block.settings.text_color }};
    --breadcrumb-link-color: {{ block.settings.link_color }};
    --breadcrumb-delimiter-color: {{ block.settings.delimiter_color }};
    {% if block.settings.show_on_mobile == false %}
      --breadcrumb-mobile-display: none;
    {% endif %}
    {% if block.settings.show_on_desktop == false %}
      --breadcrumb-desktop-display: none;
    {% endif %}
  "
>
  <ol class="app-breadcrumbs__list">
    <li class="app-breadcrumbs__item">
      <a href="/" class="app-breadcrumbs__link">{{ home_text }}</a>
    </li>

    <!-- Page-type-specific crumbs will go here -->

  </ol>
</nav>
Enter fullscreen mode Exit fullscreen mode

We pass merchant settings as CSS custom properties through inline styles. This keeps the Liquid template clean and gives us a single place to consume settings in CSS. The aria-label="Breadcrumb" attribute tells screen readers this is a breadcrumb navigation landmark.

Handling Each Page Type

Now let's fill in the breadcrumb trail for each page type. We'll use Shopify's template.name and template.suffix to detect where we are.

Product Pages

Product pages are the most common breadcrumb use case. When a customer arrives at a product from a collection, Shopify makes the referring collection available through collection. If no collection context exists (e.g., the customer arrived via a direct link), we fall back to the product's first collection:

{% if template.name == 'product' %}
  {% if collection %}
    {% assign current_collection = collection %}
  {% elsif product.collections.size > 0 %}
    {% assign current_collection = product.collections[0] %}
  {% endif %}

  {% if current_collection %}
    <li class="app-breadcrumbs__item">
      <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
      <a href="{{ current_collection.url }}" class="app-breadcrumbs__link">
        {{ current_collection.title }}
      </a>
    </li>
  {% endif %}

  <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <span class="app-breadcrumbs__current">{{ product.title }}</span>
  </li>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

A few things to note:

  • The collection object is automatically populated by Shopify when a customer navigates from a collection page to a product. This is why breadcrumbs on product pages can show the correct parent collection contextually.
  • We use aria-current="page" on the last crumb to indicate the current page to assistive technologies.
  • The current page (last crumb) is rendered as a <span>, not a link — linking to the current page is bad UX and bad for accessibility.

Collection Pages

Collection pages are straightforward — just Home > Collection Name:

{% if template.name == 'collection' %}
  <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <span class="app-breadcrumbs__current">{{ collection.title }}</span>
  </li>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Blog Article Pages

Articles live under a blog, so the trail is Home > Blog > Article:

{% if template.name == 'article' %}
  <li class="app-breadcrumbs__item">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <a href="{{ blog.url }}" class="app-breadcrumbs__link">
      {{ blog.title }}
    </a>
  </li>
  <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <span class="app-breadcrumbs__current">{{ article.title }}</span>
  </li>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Blog Pages

When viewing the blog index (list of articles), the trail is just Home > Blog:

{% if template.name == 'blog' %}
  <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <span class="app-breadcrumbs__current">{{ blog.title }}</span>
  </li>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Static Pages

Static pages (About Us, Contact, etc.) follow the same simple pattern:

{% if template.name == 'page' %}
  <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <span class="app-breadcrumbs__current">{{ page.title }}</span>
  </li>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Search and Cart Pages

Search results and cart pages deserve breadcrumbs too:

{% if template.name == 'search' %}
  <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <span class="app-breadcrumbs__current">Search</span>
  </li>
{% endif %}

{% if template.name == 'cart' %}
  <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
    <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
    <span class="app-breadcrumbs__current">Cart</span>
  </li>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Styling the Breadcrumbs

Create extensions/smart-breadcrumbs/assets/breadcrumbs.css:

.app-breadcrumbs {
  padding: 12px 0;
  font-size: var(--breadcrumb-font-size, 14px);
  color: var(--breadcrumb-text-color, #666);
  line-height: 1.5;
}

.app-breadcrumbs__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: 4px;
}

.app-breadcrumbs__item {
  display: flex;
  align-items: center;
  gap: 4px;
}

.app-breadcrumbs__link {
  color: var(--breadcrumb-link-color, #333);
  text-decoration: none;
  transition: opacity 0.2s ease;
}

.app-breadcrumbs__link:hover {
  opacity: 0.7;
  text-decoration: underline;
}

.app-breadcrumbs__delimiter {
  color: var(--breadcrumb-delimiter-color, #999);
  user-select: none;
}

.app-breadcrumbs__current {
  color: var(--breadcrumb-text-color, #666);
}

/* Responsive visibility */
@media screen and (max-width: 749px) {
  .app-breadcrumbs {
    display: var(--breadcrumb-mobile-display, block);
  }
}

@media screen and (min-width: 750px) {
  .app-breadcrumbs {
    display: var(--breadcrumb-desktop-display, block);
  }
}
Enter fullscreen mode Exit fullscreen mode

Key styling decisions:

  • Flexbox with flex-wrap — breadcrumbs wrap gracefully on narrow screens instead of overflowing.
  • CSS custom properties — all colors and sizes come from the Liquid template's inline styles, which read from merchant settings. This means zero JavaScript is needed for customization.
  • Responsive visibility — the --breadcrumb-mobile-display and --breadcrumb-desktop-display variables control whether breadcrumbs appear on each device type. By default, they show on both.
  • Minimal footprint — no shadows, no borders, no heavy decoration. Breadcrumbs should blend into the store's existing design, not compete with it.

Adding JSON-LD Structured Data

Breadcrumbs are one of the most impactful structured data types you can add to a Shopify store. When Google detects valid breadcrumb markup, it can replace the raw URL in search results with a formatted breadcrumb trail — making your listings more readable and clickable.

We'll output JSON-LD structured data alongside the visible breadcrumbs. JSON-LD is Google's preferred format for structured data, and it's cleanly separated from the HTML markup.

Add this right after the closing </nav> tag in breadcrumbs.liquid:

{% comment %} JSON-LD Structured Data {% endcomment %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": {{ home_text | json }},
      "item": "{{ shop.url }}"
    }

    {% if template.name == 'product' %}
      {% if current_collection %}
        ,{
          "@type": "ListItem",
          "position": 2,
          "name": {{ current_collection.title | json }},
          "item": "{{ shop.url }}{{ current_collection.url }}"
        }
        ,{
          "@type": "ListItem",
          "position": 3,
          "name": {{ product.title | json }}
        }
      {% else %}
        ,{
          "@type": "ListItem",
          "position": 2,
          "name": {{ product.title | json }}
        }
      {% endif %}
    {% endif %}

    {% if template.name == 'collection' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ collection.title | json }}
      }
    {% endif %}

    {% if template.name == 'article' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ blog.title | json }},
        "item": "{{ shop.url }}{{ blog.url }}"
      }
      ,{
        "@type": "ListItem",
        "position": 3,
        "name": {{ article.title | json }}
      }
    {% endif %}

    {% if template.name == 'blog' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ blog.title | json }}
      }
    {% endif %}

    {% if template.name == 'page' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ page.title | json }}
      }
    {% endif %}
  ]
}
</script>
Enter fullscreen mode Exit fullscreen mode

A few important details:

  • The last item has no item property — per Google's structured data guidelines, the last breadcrumb (the current page) should omit the item URL.
  • We use the | json Liquid filter to safely escape product and collection titles. This prevents broken JSON when titles contain quotes, ampersands, or other special characters.
  • shop.url provides the full domain — Shopify's collection.url and blog.url return relative paths (e.g., /collections/shoes), so we prepend shop.url to create fully qualified URLs that search engines expect.

You can validate your structured data using Google's Rich Results Test after deploying.


Making It Configurable from the Theme Editor

Now let's add the {% schema %} block that defines how merchants interact with the breadcrumb settings in the theme editor. This goes at the bottom of breadcrumbs.liquid:

{% schema %}
{
  "name": "Breadcrumbs",
  "target": "section",
  "settings": [
    {
      "type": "text",
      "id": "home_text",
      "label": "Home link text",
      "default": "Home"
    },
    {
      "type": "text",
      "id": "delimiter",
      "label": "Delimiter",
      "default": "/"
    },
    {
      "type": "range",
      "id": "font_size",
      "label": "Font size",
      "min": 10,
      "max": 24,
      "step": 1,
      "default": 14,
      "unit": "px"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Current page text color",
      "default": "#666666"
    },
    {
      "type": "color",
      "id": "link_color",
      "label": "Link color",
      "default": "#333333"
    },
    {
      "type": "color",
      "id": "delimiter_color",
      "label": "Delimiter color",
      "default": "#999999"
    },
    {
      "type": "checkbox",
      "id": "show_on_mobile",
      "label": "Show on mobile",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "show_on_desktop",
      "label": "Show on desktop",
      "default": true
    }
  ]
}
{% endschema %}
Enter fullscreen mode Exit fullscreen mode

This schema gives merchants full control over:

  • Home link text — Some stores prefer "Top" or "Store" instead of "Home"
  • Delimiter — Choose between /, >, , , or any custom character
  • Font size — A range slider from 10px to 24px
  • Colors — Independent control over link color, current page text color, and delimiter color
  • Responsive visibility — Toggle breadcrumbs on/off for mobile and desktop independently

The "target": "section" property tells Shopify this block can be added to any section in the theme editor — merchants simply click "Add block" and search for "Breadcrumbs."


Complete File

Here's the entire extensions/smart-breadcrumbs/blocks/breadcrumbs.liquid file in one piece. You can copy this directly into your project:

{{ 'breadcrumbs.css' | asset_url | stylesheet_tag }}

{% assign delimiter = block.settings.delimiter %}
{% assign home_text = block.settings.home_text %}

{% comment %} Determine collection context for product pages {% endcomment %}
{% if template.name == 'product' %}
  {% if collection %}
    {% assign current_collection = collection %}
  {% elsif product.collections.size > 0 %}
    {% assign current_collection = product.collections[0] %}
  {% endif %}
{% endif %}

<nav
  class="app-breadcrumbs"
  aria-label="Breadcrumb"
  style="
    --breadcrumb-font-size: {{ block.settings.font_size }}px;
    --breadcrumb-text-color: {{ block.settings.text_color }};
    --breadcrumb-link-color: {{ block.settings.link_color }};
    --breadcrumb-delimiter-color: {{ block.settings.delimiter_color }};
    {% if block.settings.show_on_mobile == false %}
      --breadcrumb-mobile-display: none;
    {% endif %}
    {% if block.settings.show_on_desktop == false %}
      --breadcrumb-desktop-display: none;
    {% endif %}
  "
>
  <ol class="app-breadcrumbs__list">
    {%- comment -%} Home link (always present) {%- endcomment -%}
    <li class="app-breadcrumbs__item">
      <a href="/" class="app-breadcrumbs__link">{{ home_text }}</a>
    </li>

    {%- comment -%} Product pages: Home > Collection > Product {%- endcomment -%}
    {% if template.name == 'product' %}
      {% if current_collection %}
        <li class="app-breadcrumbs__item">
          <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
          <a href="{{ current_collection.url }}" class="app-breadcrumbs__link">
            {{ current_collection.title }}
          </a>
        </li>
      {% endif %}
      <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <span class="app-breadcrumbs__current">{{ product.title }}</span>
      </li>
    {% endif %}

    {%- comment -%} Collection pages: Home > Collection {%- endcomment -%}
    {% if template.name == 'collection' %}
      <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <span class="app-breadcrumbs__current">{{ collection.title }}</span>
      </li>
    {% endif %}

    {%- comment -%} Article pages: Home > Blog > Article {%- endcomment -%}
    {% if template.name == 'article' %}
      <li class="app-breadcrumbs__item">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <a href="{{ blog.url }}" class="app-breadcrumbs__link">
          {{ blog.title }}
        </a>
      </li>
      <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <span class="app-breadcrumbs__current">{{ article.title }}</span>
      </li>
    {% endif %}

    {%- comment -%} Blog pages: Home > Blog {%- endcomment -%}
    {% if template.name == 'blog' %}
      <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <span class="app-breadcrumbs__current">{{ blog.title }}</span>
      </li>
    {% endif %}

    {%- comment -%} Static pages: Home > Page {%- endcomment -%}
    {% if template.name == 'page' %}
      <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <span class="app-breadcrumbs__current">{{ page.title }}</span>
      </li>
    {% endif %}

    {%- comment -%} Search page: Home > Search {%- endcomment -%}
    {% if template.name == 'search' %}
      <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <span class="app-breadcrumbs__current">Search</span>
      </li>
    {% endif %}

    {%- comment -%} Cart page: Home > Cart {%- endcomment -%}
    {% if template.name == 'cart' %}
      <li class="app-breadcrumbs__item app-breadcrumbs__item--current" aria-current="page">
        <span class="app-breadcrumbs__delimiter">{{ delimiter }}</span>
        <span class="app-breadcrumbs__current">Cart</span>
      </li>
    {% endif %}
  </ol>
</nav>

{% comment %} JSON-LD Structured Data {% endcomment %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": {{ home_text | json }},
      "item": "{{ shop.url }}"
    }

    {% if template.name == 'product' %}
      {% if current_collection %}
        ,{
          "@type": "ListItem",
          "position": 2,
          "name": {{ current_collection.title | json }},
          "item": "{{ shop.url }}{{ current_collection.url }}"
        }
        ,{
          "@type": "ListItem",
          "position": 3,
          "name": {{ product.title | json }}
        }
      {% else %}
        ,{
          "@type": "ListItem",
          "position": 2,
          "name": {{ product.title | json }}
        }
      {% endif %}
    {% endif %}

    {% if template.name == 'collection' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ collection.title | json }}
      }
    {% endif %}

    {% if template.name == 'article' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ blog.title | json }},
        "item": "{{ shop.url }}{{ blog.url }}"
      }
      ,{
        "@type": "ListItem",
        "position": 3,
        "name": {{ article.title | json }}
      }
    {% endif %}

    {% if template.name == 'blog' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ blog.title | json }}
      }
    {% endif %}

    {% if template.name == 'page' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": {{ page.title | json }}
      }
    {% endif %}
  ]
}
</script>

{% schema %}
{
  "name": "Breadcrumbs",
  "target": "section",
  "settings": [
    {
      "type": "text",
      "id": "home_text",
      "label": "Home link text",
      "default": "Home"
    },
    {
      "type": "text",
      "id": "delimiter",
      "label": "Delimiter",
      "default": "/"
    },
    {
      "type": "range",
      "id": "font_size",
      "label": "Font size",
      "min": 10,
      "max": 24,
      "step": 1,
      "default": 14,
      "unit": "px"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Current page text color",
      "default": "#666666"
    },
    {
      "type": "color",
      "id": "link_color",
      "label": "Link color",
      "default": "#333333"
    },
    {
      "type": "color",
      "id": "delimiter_color",
      "label": "Delimiter color",
      "default": "#999999"
    },
    {
      "type": "checkbox",
      "id": "show_on_mobile",
      "label": "Show on mobile",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "show_on_desktop",
      "label": "Show on desktop",
      "default": true
    }
  ]
}
{% endschema %}
Enter fullscreen mode Exit fullscreen mode

And here's the complete extensions/smart-breadcrumbs/assets/breadcrumbs.css:

.app-breadcrumbs {
  padding: 12px 0;
  font-size: var(--breadcrumb-font-size, 14px);
  color: var(--breadcrumb-text-color, #666);
  line-height: 1.5;
}

.app-breadcrumbs__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: 4px;
}

.app-breadcrumbs__item {
  display: flex;
  align-items: center;
  gap: 4px;
}

.app-breadcrumbs__link {
  color: var(--breadcrumb-link-color, #333);
  text-decoration: none;
  transition: opacity 0.2s ease;
}

.app-breadcrumbs__link:hover {
  opacity: 0.7;
  text-decoration: underline;
}

.app-breadcrumbs__delimiter {
  color: var(--breadcrumb-delimiter-color, #999);
  user-select: none;
}

.app-breadcrumbs__current {
  color: var(--breadcrumb-text-color, #666);
}

/* Responsive visibility */
@media screen and (max-width: 749px) {
  .app-breadcrumbs {
    display: var(--breadcrumb-mobile-display, block);
  }
}

@media screen and (min-width: 750px) {
  .app-breadcrumbs {
    display: var(--breadcrumb-desktop-display, block);
  }
}
Enter fullscreen mode Exit fullscreen mode

Things to Watch Out For

Collection Context on Product Pages

The collection Liquid object is only available when a customer navigates to a product from a collection page. If someone lands on a product via a direct link, search, or a product recommendation, collection will be nil. Our code handles this by falling back to product.collections[0], but be aware that the breadcrumb trail might show a different collection than the one the customer expects if the product belongs to multiple collections.

Delimiter Characters and Encoding

Common delimiter choices include /, >, (single right-pointing angle quotation mark), and . All of these work fine in both the visible breadcrumbs and JSON-LD. However, if a merchant enters an HTML entity like &gt;, Liquid will output it as the literal string &gt; inside the JSON-LD <script> tag, which is technically valid but messy. Stick to actual characters rather than HTML entities.

JSON-LD Validation

Always test your structured data output with Google's Rich Results Test after deploying. Common issues include:

  • Missing item property on intermediate breadcrumbs — every breadcrumb except the last one needs a URL
  • Relative URLs — Google expects fully qualified URLs, which is why we prepend shop.url
  • Special characters in titles — the | json filter handles this, but double-check with products that have quotes or ampersands in their names

Theme Editor Placement

Merchants should place the breadcrumb block at the top of the page template, typically inside the first section. Since the block uses "target": "section", it can be added to any section — but placement at the top of the main content area is the standard convention for breadcrumbs.

Performance

This implementation requires zero JavaScript — everything is handled by Liquid and CSS. The only assets loaded are a single small CSS file. This means breadcrumbs add virtually no overhead to page load time, which is important for Shopify stores where every millisecond of load time affects conversion rates.


Conclusion

We've built a production-ready breadcrumb navigation block that handles every major Shopify page type, outputs SEO-friendly JSON-LD structured data, and gives merchants full visual control from the theme editor — all without a single line of JavaScript.

The key takeaways:

  • Theme App Extensions are the cleanest way to deliver UI components to Shopify merchants — no theme code editing, no ScriptTags, no leftovers on uninstall
  • JSON-LD structured data for breadcrumbs can directly improve how your store appears in Google search results
  • CSS custom properties are a powerful pattern for passing Liquid settings to stylesheets without JavaScript
  • Semantic HTML (<nav>, <ol>, aria-label, aria-current) makes breadcrumbs accessible to all users

If you want breadcrumb functionality without building it yourself, check out UR: Smart Breadcrumbs — a no-code solution from UnReact that covers all of this and more.


UR: Smart Breadcrumbs

If you'd rather skip the implementation and add breadcrumbs to your store right away, UR: Smart Breadcrumbs by UnReact is a no-code Shopify app that delivers everything covered in this tutorial — and then some.

Key features:

  • No-code setup — add breadcrumbs to any Online Store 2.0 theme with one click from the theme editor
  • Customizable delimiters — choose from common separators or set your own custom character
  • Full color and typography control — adjust font size, link color, text color, and delimiter color to match your store's design
  • Responsive visibility toggles — show or hide breadcrumbs independently on mobile and desktop
  • Home link customization — change the home link text and positioning to fit your store's navigation style
  • Multi-language support — supports 20+ languages including English, Japanese, French, German, Spanish, and more

It's available on the Shopify App Store with a 7-day free trial.


References

Top comments (0)