All posts

Tutorial

How to add breadcrumbs to a Shopify theme with Liquid (and the SEO schema)

Build accessible breadcrumb navigation in Shopify with Liquid: the template-aware markup, the routes object, BreadcrumbList structured data for Google, and the accessibility details most copy-pasted versions miss. No app required.

Bas Lefeber

Founder, learnshopify.dev · June 12, 2026 · 4 min read

Breadcrumbs are the small trail near the top of a page that shows where you are: Home / Coffee / Ethiopian Yirgacheffe. They help shoppers climb back up a category without hunting for the back button, and, done right, they give Google a clean map of your site that can show up as a breadcrumb trail in the search result itself.

This guide builds them properly in Liquid: markup that adapts to the page type, the routes object so links never break, the BreadcrumbList structured data that earns the rich result, and the accessibility attributes that most snippets skip. No app, no monthly fee.

A Shopify product page with a Home / Single-origin / Ethiopian Yirgacheffe breadcrumb trail above the product title
Breadcrumbs in context on a product page: a clear path back up to the parent collection.

Where breadcrumbs go

Breadcrumbs belong at the top of the main content, just under the header, on product and collection pages (and optionally pages and blog articles). You render them from a snippet so the same trail logic lives in one place. In Shopify's Horizon and other Online Store 2.0 themes, you drop the snippet into the product and collection sections.

The breadcrumb snippet

Create snippets/breadcrumbs.liquid and render it with {% render 'breadcrumbs' %} from your product and collection sections. Here is the whole thing:

snippets/breadcrumbs.liquid
<nav class="breadcrumbs" aria-label="Breadcrumb">  <a href="{{ routes.root_url }}">Home</a>   {% if template.name == 'product' %}    {% if product.collections.size > 0 %}      {% assign breadcrumb_collection = product.collections.first %}      <span aria-hidden="true">/</span>      <a href="{{ breadcrumb_collection.url }}">{{ breadcrumb_collection.title }}</a>    {% endif %}    <span aria-hidden="true">/</span>    <span aria-current="page">{{ product.title }}</span>   {% elsif template.name == 'collection' %}    <span aria-hidden="true">/</span>    <span aria-current="page">{{ collection.title }}</span>  {% endif %}</nav>

How it works

  • routes.root_url is Shopify's safe way to link to the homepage. Never hardcode /: the routes object keeps links correct across locales and markets, where the homepage might be /en-ca rather than /.
  • template.name tells you the page type (product, collection, page...), so one snippet renders the right trail everywhere instead of guessing from the URL.
  • product.collections.first picks a parent collection for the product trail. A product can belong to several collections; first is a sensible default, and below we cover choosing a smarter one.
  • aria-current="page" marks the final crumb (the current page) for screen readers, and it is not a link because you are already there. MDN documents aria-current as the correct attribute for exactly this.

Pick a deliberate parent collection

If your products live in many collections, first can be arbitrary. A common upgrade is to read a chosen collection from a metafield (for example product.metafields.custom.primary_collection) and fall back to product.collections.first when it is empty.

Add the BreadcrumbList structured data (the SEO part)

The visible trail helps shoppers. To help Google show a breadcrumb trail in the search result, you also emit BreadcrumbList structured data as JSON-LD. This is the step that turns breadcrumbs from a UX nicety into an SEO win. Add this next to the snippet above:

snippets/breadcrumbs.liquid (append)
<script type="application/ld+json">{  "@context": "https://schema.org",  "@type": "BreadcrumbList",  "itemListElement": [    {      "@type": "ListItem",      "position": 1,      "name": "Home",      "item": {{ shop.url | json }}    }    {% if template.name == 'product' and product.collections.size > 0 %}      {% assign breadcrumb_collection = product.collections.first %}    ,{      "@type": "ListItem",      "position": 2,      "name": {{ breadcrumb_collection.title | json }},      "item": {{ shop.url | append: breadcrumb_collection.url | json }}    },    {      "@type": "ListItem",      "position": 3,      "name": {{ product.title | json }}    }    {% elsif template.name == 'collection' %}    ,{      "@type": "ListItem",      "position": 2,      "name": {{ collection.title | json }}    }    {% endif %}  ]}</script>

Always pipe dynamic values through json

Notice every value uses the json filter ({{ product.title | json }}). A product titled The "Daily" Blend contains a quote that would break your JSON and invalidate the structured data. The json filter escapes it correctly and adds the surrounding quotes, so never wrap these values in your own "...".

The last item intentionally omits item (the URL). Google's breadcrumb structured-data guidelines say the final breadcrumb (the current page) does not need a URL. After you ship, paste a product URL into the Rich Results Test to confirm the breadcrumb is detected.

Minimal styling

Keep it understated so it reads as navigation, not a feature. This inherits your theme's font and just handles spacing and the muted look:

assets/breadcrumbs.css
.breadcrumbs {  display: flex;  flex-wrap: wrap;  align-items: center;  gap: 0.5rem;  font-size: 0.85rem;  color: rgba(0, 0, 0, 0.55);}.breadcrumbs a {  color: inherit;  text-decoration: none;}.breadcrumbs a:hover {  text-decoration: underline;}.breadcrumbs [aria-current="page"] {  color: rgba(0, 0, 0, 0.9);}

What AI tools get wrong here

Ask an AI assistant for Shopify breadcrumbs and the markup usually looks fine but quietly fails in production. The two recurring mistakes:

  • Hardcoded URLs: linking to / or building collection URLs by hand instead of using routes.root_url and collection.url. This breaks the moment the store adds a second market or locale.
  • Unescaped JSON-LD: dropping {{ product.title }} straight into the structured data without json, so any product with a quote or apostrophe ships invalid schema that Google silently ignores.

Spotting those on sight is the kind of judgment that stays valuable when AI writes the first draft. If you want to build that instinct deliberately, that is the whole point of the way we teach Shopify development.

Learn this properly · free lesson

The header menu is data: rendering a linklist

Breadcrumbs are one slice of theme navigation. Build a real menu from a Shopify linklist in our interactive editor, against a live store, with a reviewer checking your work.

Try this lesson — free

Wrapping up

One snippet, the routes object so links survive multi-market stores, a BreadcrumbList block so Google can render the trail, and aria-current so it is accessible. That is the difference between breadcrumbs that look right and breadcrumbs that actually do their job in search and for every shopper.

Frequently asked questions

Does Shopify have built-in breadcrumbs?

Some themes include a breadcrumb snippet, but Shopify does not add breadcrumbs automatically and many themes omit them. Adding the Liquid snippet in this guide gives you full control over the markup, styling, and the BreadcrumbList structured data that powers the search-result trail.

How do I add breadcrumb structured data in Shopify?

Emit a BreadcrumbList JSON-LD block alongside your visible breadcrumbs, building the item list from template.name, product.collections.first, and the current product or collection. Pipe every dynamic value through the json filter so quotes and apostrophes do not break the markup, then verify with Google's Rich Results Test.

Why should I use routes.root_url instead of just /?

On multi-market or multi-language stores the homepage may live at a localized path like /en-ca rather than /. routes.root_url always resolves to the correct homepage for the current context, so your breadcrumbs and links never point to the wrong locale.

Which collection should the product breadcrumb point to?

A product can belong to many collections, so product.collections.first is a reasonable default. For deliberate trails, store a chosen collection in a product metafield and fall back to product.collections.first when it is not set.

On the launch list

Get updates on the platform.

Same waitlist as the homepage. New posts plus a heads-up when v1 launches. No drip, no spam.

One email when a new module ships, one when v1 launches. No drip sequence, no spam. Unsubscribe anytime.

LiquidSEONavigation

Keep going in the curriculum