How to Add Dynamic Breadcrumb Schema in Shopify (JSON-LD)

Share at:
How to add dynamic breadcrumb schema in Shopify requires inserting JSON-LD structured data that pulls navigation paths automatically from Shopify’s template variables. This guide from HiAgency covers implementation for product pages, collection pages, and blog posts using Liquid code that adapts to your site structure without manual updates.
Developer implementing dynamic breadcrumb schema in Shopify store navigation structure
Breadcrumb schema displays your site hierarchy in search results as clickable navigation paths. Dynamic implementation ensures the markup updates automatically when you reorganize collections, add new categories, or restructure your store navigation. Dynamic breadcrumb schema generates navigation paths automatically from Shopify’s database using Liquid template variables. Static breadcrumbs require hardcoding each URL and title, breaking when you rename collections or move products between categories. Website hierarchy diagram showing dynamic breadcrumb navigation levels Dynamic implementation benefits:
  • Automatic updates – Schema reflects current collection names and product locations without code changes
  • Multi-level support – Handles nested collections (Department > Category > Subcategory) automatically
  • Template flexibility – Works across different page types (products, collections, pages, blogs) with single implementation
  • Maintenance-free – No manual edits needed when restructuring site navigation
Shopify provides template objects (product.collections, collection.url, page.title) that contain current page information. Breadcrumb schema pulls from these objects to generate accurate navigation paths. Product page breadcrumbs show the path from homepage through collection to the specific product. Implementation uses the product’s collection assignment to build the hierarchy. Ecommerce product page displaying breadcrumb navigation above product title Navigate to theme code editor:
  1. Go to Online Store > Themes
  2. Click Actions > Edit code
  3. Open Templates/product.liquid (or sections/main-product.liquid for OS 2.0 themes)
Add this code near the closing </body> tag or in the template head:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "{{ shop.url }}"
    },
    {% if product.collections.size > 0 %}
    {
      "@type": "ListItem",
      "position": 2,
      "name": "{{ product.collections.first.title }}",
      "item": "{{ shop.url }}{{ product.collections.first.url }}"
    },
    {% endif %}
    {
      "@type": "ListItem",
      "position": {% if product.collections.size > 0 %}3{% else %}2{% endif %},
      "name": "{{ product.title }}",
      "item": "{{ shop.url }}{{ product.url }}"
    }
  ]
}
</script>
This code creates a three-level breadcrumb: Home > Collection > Product. The {% if %} conditional handles products not assigned to collections. Products assigned to multiple collections need logic to select the most relevant collection for breadcrumbs:
{% assign current_collection = collection %}
{% if current_collection == blank and product.collections.size > 0 %}
  {% assign current_collection = product.collections.first %}
{% endif %}
Add this before the schema block. It prioritizes the collection from which the customer navigated to the product, falling back to the first assigned collection. Updated schema using current_collection:
{
  "@type": "ListItem",
  "position": 2,
  "name": "{{ current_collection.title }}",
  "item": "{{ shop.url }}{{ current_collection.url }}"
}
For stores with parent/child collection hierarchies (using collection metafields or tags), extend the breadcrumb logic:
{% if current_collection.metafields.custom.parent_collection %}
  {% assign parent_collection_handle = current_collection.metafields.custom.parent_collection %}
  {% assign parent_collection = collections[parent_collection_handle] %}
  
  {
    "@type": "ListItem",
    "position": 2,
    "name": "{{ parent_collection.title }}",
    "item": "{{ shop.url }}{{ parent_collection.url }}"
  },
  {
    "@type": "ListItem",
    "position": 3,
    "name": "{{ current_collection.title }}",
    "item": "{{ shop.url }}{{ current_collection.url }}"
  },
{% endif %}
This assumes you store parent collection handles in a custom metafield. Adjust the metafield namespace and key to match your setup. Collection page breadcrumbs show Home > Collection (or Home > Parent Collection > Collection for nested structures). Access collection template file:
  1. In theme code editor, open Templates/collection.liquid
  2. For OS 2.0 themes, edit sections/main-collection.liquid
Add this code to the collection template:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "{{ shop.url }}"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "{{ collection.title }}",
      "item": "{{ shop.url }}{{ collection.url }}"
    }
  ]
}
</script>
The collection object provides title and URL automatically. This creates a two-level breadcrumb for standard collections. For nested collections, add parent collection detection:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "{{ shop.url }}"
    },
    {% if collection.metafields.custom.parent_collection %}
      {% assign parent_handle = collection.metafields.custom.parent_collection %}
      {% assign parent = collections[parent_handle] %}
      {
        "@type": "ListItem",
        "position": 2,
        "name": "{{ parent.title }}",
        "item": "{{ shop.url }}{{ parent.url }}"
      },
      {
        "@type": "ListItem",
        "position": 3,
        "name": "{{ collection.title }}",
        "item": "{{ shop.url }}{{ collection.url }}"
      }
    {% else %}
      {
        "@type": "ListItem",
        "position": 2,
        "name": "{{ collection.title }}",
        "item": "{{ shop.url }}{{ collection.url }}"
      }
    {% endif %}
  ]
}
</script>
This checks for a parent_collection metafield and builds three-level breadcrumbs when present, falling back to two levels for top-level collections. Blog post breadcrumbs follow the pattern: Home > Blog > Article Title.

Step 1: Access Article Template

Open the article template file:
  1. Navigate to Templates/article.liquid
  2. For OS 2.0 themes, edit sections/main-article.liquid

Step 2: Insert Article Breadcrumb Schema

Add this code to the article template:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "{{ shop.url }}"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "{{ blog.title }}",
      "item": "{{ shop.url }}{{ blog.url }}"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "{{ article.title }}",
      "item": "{{ shop.url }}{{ article.url }}"
    }
  ]
}
</script>
The blog and article objects provide titles and URLs automatically. This creates a three-level breadcrumb for all blog posts. If your store uses multiple blogs (e.g., News, Guides, Updates), the schema adapts automatically because {{ blog.title }} pulls the current blog name:
  • Post in “News” blog: Home > News > Article Title
  • Post in “Guides” blog: Home > Guides > Article Title
No additional code needed for multi-blog support. Standard pages (About Us, Contact, FAQ) use simple two-level breadcrumbs: Home > Page Title. Access the page template:
  1. Open Templates/page.liquid
  2. For OS 2.0 themes, edit sections/main-page.liquid
Add this code to the page template:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "{{ shop.url }}"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "{{ page.title }}",
      "item": "{{ shop.url }}{{ page.url }}"
    }
  ]
}
</script>
The page object provides title and URL. This works for all standard pages automatically. Universal implementation adds breadcrumb schema to all page types from a single code block in theme.liquid. Flow chart illustrating universal dynamic breadcrumb schema logic across templates Access the main theme file:
  1. In theme code editor, open Layout/theme.liquid
  2. Locate the closing </head> or </body> tag
Add this comprehensive code block:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "{{ shop.url }}"
    }
    {% if template contains 'product' %}
      {% if product.collections.size > 0 %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": "{{ product.collections.first.title }}",
        "item": "{{ shop.url }}{{ product.collections.first.url }}"
      }
      {% endif %}
      ,{
        "@type": "ListItem",
        "position": {% if product.collections.size > 0 %}3{% else %}2{% endif %},
        "name": "{{ product.title }}",
        "item": "{{ shop.url }}{{ product.url }}"
      }
    {% elsif template contains 'collection' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": "{{ collection.title }}",
        "item": "{{ shop.url }}{{ collection.url }}"
      }
    {% elsif template contains 'article' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": "{{ blog.title }}",
        "item": "{{ shop.url }}{{ blog.url }}"
      }
      ,{
        "@type": "ListItem",
        "position": 3,
        "name": "{{ article.title }}",
        "item": "{{ shop.url }}{{ article.url }}"
      }
    {% elsif template contains 'page' %}
      ,{
        "@type": "ListItem",
        "position": 2,
        "name": "{{ page.title }}",
        "item": "{{ shop.url }}{{ page.url }}"
      }
    {% endif %}
  ]
}
</script>
This single block handles products, collections, blog posts, and pages. The template conditional determines which breadcrumb structure to generate. Wrap the entire schema block in a conditional to prevent breadcrumbs on the homepage:
{% unless template == 'index' %}
  <script type="application/ld+json">
  ...breadcrumb schema code...
  </script>
{% endunless %}
Homepages don’t need breadcrumbs since they’re the starting point of navigation. Search results pages require special handling because they don’t fit standard navigation hierarchies. Add search detection to universal schema:
{% elsif template contains 'search' %}
  ,{
    "@type": "ListItem",
    "position": 2,
    "name": "Search Results",
    "item": "{{ shop.url }}{{ request.path }}"
  }
{% endif %}
Display the actual search term in breadcrumbs:
{% elsif template contains 'search' %}
  ,{
    "@type": "ListItem",
    "position": 2,
    "name": "Search Results{% if search.terms %}: {{ search.terms }}{% endif %}",
    "item": "{{ shop.url }}{{ request.path }}"
  }
{% endif %}
This creates breadcrumbs like “Home > Search Results: running shoes” for search pages. Testing ensures dynamic breadcrumbs generate correctly across different page types. Google Rich Results Test interface validating breadcrumb structured data Visit and validate these pages:
  1. Product page – Verify collection appears in breadcrumb
  2. Collection page – Check collection title displays
  3. Blog post – Confirm blog name and article title show
  4. Standard page – Test page title appears
For each page, view source and search for “BreadcrumbList” to inspect the generated schema. Validate each page type:
  1. Go to search.google.com/test/rich-results
  2. Enter page URL
  3. Click Test URL
  4. Verify “Breadcrumb” detected in results
Check that position numbers increment correctly and item URLs are complete (include https:// and domain). Common issues with dynamic schema: Empty collection titles – Products not assigned to collections produce blank breadcrumb items. Use {% if product.collections.size > 0 %} conditionals to prevent this. Missing shop.url – URLs show as /collections/shoes instead of https://store.com/collections/shoes. Always prefix with {{ shop.url }}. Special characters in titles – Product or collection names with quotes break JSON syntax. Use | escape filter: “name”: “{{ product.title | escape }}”. Schema apps provide dynamic breadcrumb implementation without code editing. Three apps support dynamic breadcrumbs: JSON-LD for SEO:
  • Free plan available
  • Automatic breadcrumb generation for all page types
  • Handles nested collections via metafields
Schema Plus for SEO:
  • $5.99/month
  • Visual breadcrumb configurator
  • Custom breadcrumb rules per template
Smart SEO:
  • $4.99/month
  • Breadcrumb schema included with full SEO suite
  • Multi-language breadcrumb support
In app settings:
  1. Enable Breadcrumb Schema toggle
  2. Select page types to include (products, collections, blogs, pages)
  3. Configure collection priority for products in multiple collections
  4. Set homepage label (Home, Homepage, Main Page)
Verify app output:
  1. Visit different page types
  2. View page source
  3. Locate BreadcrumbList schema
  4. Check accuracy of names and URLs
Most apps generate schema dynamically using the same Liquid variables as manual implementation. Error: Position goes 1, 2, 4 or shows 2, 2, 3 Cause: Conditional logic breaks position numbering Fix: Use dynamic position calculation:
{% assign position = 1 %}
{
  "@type": "ListItem",
  "position": {{ position }},
  "name": "Home",
  "item": "{{ shop.url }}"
}
{% assign position = position | plus: 1 %}
{% if product.collections.size > 0 %}
  ,{
    "@type": "ListItem",
    "position": {{ position }},
    "name": "{{ product.collections.first.title }}",
    "item": "{{ shop.url }}{{ product.collections.first.url }}"
  }
  {% assign position = position | plus: 1 %}
{% endif %}
Increment position variable only when adding breadcrumb items. Error: “Unexpected token” or JSON parse error Cause: Extra comma after last breadcrumb item Fix: Remove trailing commas from conditional blocks:
{% if product.collections.size > 0 %}
  ,{
    "@type": "ListItem",
    "position": 2,
    "name": "{{ product.collections.first.title }}",
    "item": "{{ shop.url }}{{ product.collections.first.url }}"
  }
{% endif %}
Notice the comma before the opening brace, not after the closing brace. Error: “Missing field ‘item’ (required)” Cause: Collection or product URL is blank Fix: Always include {{ shop.url }} prefix:
"item": "{{ shop.url }}{{ collection.url }}"
Never use collection.url alone, as it returns a relative path (/collections/shoes) not a full URL.
Products assigned to multiple collections need logic to select which collection appears in breadcrumbs. Shopify doesn’t automatically determine the “primary” collection, so you must implement priority rules.
The most effective approach uses the collection context. When customers navigate from a collection page to a product, that collection should appear in breadcrumbs:
{% assign breadcrumb_collection = collection %}
{% if breadcrumb_collection == blank %}
  {% assign breadcrumb_collection = product.collections.first %}
{% endif %}
This checks if a collection context exists (customer came from a collection page). If blank, it falls back to the first assigned collection. For more control, prioritize featured collections using product metafields:
{% if product.metafields.custom.primary_collection %}
  {% assign primary_handle = product.metafields.custom.primary_collection %}
  {% assign breadcrumb_collection = collections[primary_handle] %}
{% elsif collection %}
  {% assign breadcrumb_collection = collection %}
{% else %}
  {% assign breadcrumb_collection = product.collections.first %}
{% endif %}
This hierarchy prioritizes: 1) manually assigned primary collection from metafield, 2) collection from navigation context, 3) first alphabetically assigned collection. For products with parent/child collection relationships, add logic to detect collection hierarchy and build multi-level breadcrumbs accordingly. Breadcrumb schema should not include filter or sort parameters in URLs. Google expects breadcrumbs to represent the site’s content hierarchy, not user session state or URL parameters. Incorrect implementation including filters:
"item": "{{ shop.url }}{{ collection.url }}?sort_by=price-ascending"
This creates different breadcrumb URLs for the same collection depending on how users sorted products, which confuses search engines about your site structure. Correct implementation uses canonical collection URLs:
"item": "{{ shop.url }}{{ collection.url }}"
Even if customers arrived at a product through filtered collection views (e.g., /collections/shoes?color=blue), breadcrumbs should reference the base collection URL (/collections/shoes). For breadcrumb visual display (the actual HTML breadcrumbs shown to users), you can include filter context. But the structured data schema should always point to canonical URLs without parameters. Theme.liquid placement centralizes breadcrumb schema in one location, making maintenance easier but requiring more complex conditionals to handle different page types. Individual template placement separates schema by page type, simplifying logic but requiring updates across multiple files. Choose theme.liquid when:
  • You want single-point maintenance for all breadcrumbs
  • Your store uses consistent navigation patterns across page types
  • You’re comfortable with Liquid conditionals ({% if template contains ‘product’ %})
Choose individual templates when:
  • Different page types need unique breadcrumb logic
  • You prefer simpler, template-specific code
  • You’re implementing breadcrumbs gradually across page types
Best practice: Start with individual templates to perfect breadcrumb logic for each page type, then consolidate into theme.liquid once patterns are established. This approach minimizes debugging complexity. For OS 2.0 themes, template files often just reference sections, so breadcrumb schema must go in section files (sections/main-product.liquid) rather than template files. Check your theme architecture before deciding placement. Dynamic breadcrumb schema works with Shopify Markets but requires language-aware implementation to display translated collection and product names in breadcrumbs. Shopify’s built-in translation system provides localized values for product.title and collection.title automatically when using Markets. Your existing dynamic breadcrumb code adapts to the current language without modifications:
{
  "@type": "ListItem",
  "position": 2,
  "name": "{{ collection.title }}",
  "item": "{{ shop.url }}{{ collection.url }}"
}
This outputs “Zapatos” for Spanish customers viewing a shoes collection, and “Shoes” for English customers, because {{ collection.title }} returns the translated value based on the customer’s language context. URLs must include the language prefix for non-default languages. Shopify’s {{ collection.url }} handles this automatically:
  • English (default): /collections/shoes
  • Spanish: /es/collections/zapatos
  • French: /fr/collections/chaussures
For stores using third-party translation apps (Langify, Weglot), verify that the app’s Liquid filters apply to breadcrumb schema. Some apps require explicit translation filters:
"name": "{{ collection.title | t }}"
Check your translation app’s documentation for schema compatibility. Most modern apps handle JSON-LD structured data automatically.
Dynamic breadcrumb schema requires updates only when Shopify changes Liquid template variables or you restructure your site navigation hierarchy. The code itself updates breadcrumb content automatically as you add products and collections.
Review breadcrumb implementation quarterly to ensure: Liquid variables remain valid – Shopify occasionally deprecates template objects or changes syntax (like the OS 2.0 transition). Check Shopify’s developer changelog for breaking changes. Navigation structure matches breadcrumb logic – If you add collection hierarchies or new page types (lookbook pages, landing pages), update breadcrumb conditionals to include these templates. Schema.org specifications stay current – BreadcrumbList schema is stable, but Google occasionally updates required or recommended properties. Monitor Search Console for warnings. Immediate updates needed when:
  • Migrating to OS 2.0 theme structure – Template and section architecture changes require relocating breadcrumb code
  • Implementing nested collections – Add parent collection logic to support multi-level breadcrumbs
  • Adding new template types – Create breadcrumb patterns for custom page templates
Unlike static breadcrumbs that break when you rename collections, dynamic breadcrumbs maintain accuracy without manual edits. The “dynamic” aspect means minimal ongoing maintenance.
Need expert help with Shopify SEO? HiAgency’s technical SEO specialists implement schema markup, optimize site architecture, and deliver data-driven strategies that improve organic visibility. Get in touch to discuss your Shopify store’s SEO performance.
Share at:

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 15

No votes so far! Be the first to rate this post.

HiAgency
HiAgency
HiAgency is a digital marketing and web development agency specializing in SEO optimization and content management solutions. We help businesses improve their online visibility through technical implementations, user experience enhancements, and strategic content organization.

Leave the first comment