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.

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:
<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_urlis 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-carather than/.template.nametells you the page type (product,collection,page...), so one snippet renders the right trail everywhere instead of guessing from the URL.product.collections.firstpicks a parent collection for the product trail. A product can belong to several collections;firstis 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:
<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:
.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 usingroutes.root_urlandcollection.url. This breaks the moment the store adds a second market or locale. - Unescaped JSON-LD: dropping
{{ product.title }}straight into the structured data withoutjson, 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 — freeWrapping 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.
Keep going in the curriculum
