# BreadcrumbList schema: the trail Google shows instead of your URL

> The exact JSON-LD shape for BreadcrumbList, the rules Google applies to positions, names and URLs, how the trail appears in results, and the errors that make it silently ineligible.

Updated 2026-09-25 · Structured data · HTML version: https://getreport.app/guides/breadcrumblist-schema

Under a search result Google prints either the page's URL or a readable path: `example-shop.hr › Shoes › Trail running`. The path comes from `BreadcrumbList` structured data on the page, and it is one of the few rich results almost any site qualifies for. It is also one of the easiest to get subtly wrong: a missing `position`, a relative URL or an unescaped quote makes the whole list ineligible without any error in the browser. This guide covers the JSON-LD shape line by line, the rules Google applies, how the trail is rendered, and how to test it. For the visible navigation half, see [Breadcrumbs: navigation and structured data together](https://getreport.app/guides/breadcrumbs-navigation-and-structured-data).

## Quick answer

- One `BreadcrumbList` whose `itemListElement` is an array of `ListItem` objects, each with `position` (1, 2, 3…), `name` and `item` (the absolute URL). The last item may omit `item`.
- Order is hierarchy, not click path: home, section, subsection, page. Positions increase by one with no gaps or repeats.
- The names and order must match the trail visible on the page.
- A page in two categories may carry two lists; each list is one trail.
- Run the [schema validator](https://getreport.app/tools/schema-validator): "Eligible for rich results: BreadcrumbList" is the pass.

## Why BreadcrumbList matters

The path under a result does two things. It tells the searcher where the page sits before they click, which lifts clicks on deep pages whose URLs mean nothing (`/p/48213/`), and it tells Google how your site is structured, which helps it group pages by section. Sites with a trail in their listings look organised; sites without one show whatever the URL happens to be, including `?id=` parameters and dates.

Google reads the trail from `BreadcrumbList` structured data, not from the visible links, so a site with a perfectly good breadcrumb bar and no JSON-LD gets the URL. The reverse, JSON-LD with no visible trail, is worse: Google's guidelines require the structured data to describe content the visitor can see, and markup for an invisible trail can be ignored or, if it misleads, penalised.

How the trail renders has changed several times. On desktop, the listing shows the domain and the trail names separated by `›`. On mobile, Google has at times shown the trail and at times shown only the site name; the markup is the same either way, so add it once and let Google decide the rendering.

## How getReport checks it

> **Free tool:** [Schema markup validator](https://getreport.app/tools/schema-validator): Find and validate JSON-LD and microdata on any page: parse errors, missing required properties, rich-result eligibility and duplicate entities, with the fix for each.

Two modules look at breadcrumbs. The SEO audit answers "is there a trail at all?": it looks for a `<nav>` whose `aria-label` contains "breadcrumb", an element with a class containing "breadcrumb", `BreadcrumbList` microdata, or a JSON-LD script mentioning `BreadcrumbList`, and reports which it found. That finding is informational, with no weight, because a home page or a landing page legitimately has no trail.

> **Check: Breadcrumb navigation found.** Breadcrumbs show visitors where they are and let Google show a readable path instead of the URL in desktop results. Most useful on shops and sites more than two levels deep.
>
> 1. Add a breadcrumb trail in a <nav aria-label="Breadcrumb"> and describe it with BreadcrumbList structured data.

The schema module validates the list itself. It parses every JSON-LD block, including lists inside a `@graph`, and for each `BreadcrumbList` checks that `itemListElement` is present and that every `ListItem` has a `position` and a `name` (a `name` on a nested `item` object also counts) and, for every element except the last, an `item` URL. Anything missing is listed under required properties with its path (`itemListElement[0].item`), and the eligibility finding then names `BreadcrumbList` as not eligible.

> **Check: Rich result eligibility.** Rich results (price, stars, breadcrumbs or event dates under your listing) make it bigger and earn more clicks than a plain blue link. An entity with missing required properties does not qualify.
>
> 1. Fix the properties listed under "required properties missing" first; eligibility follows automatically.
> 2. Test the page with Google's Rich Results Test after the change.

> **Check: JSON-LD syntax.** A JSON-LD block with a syntax error is ignored completely, so Google reads no structured data from it. One stray comma or missing quote costs the whole block.
>
> 1. Open the technical detail to see the parser message and the block number.
> 2. Paste the block into a JSON validator, fix the character it points at, and run this check again.

![The schema validation panel with the rich-result eligibility row listing BreadcrumbList as eligible, the parse finding passing, and the detected types row showing BreadcrumbList next to the page's other entities](https://getreport.app/guides/img/breadcrumblist-schema/schema-panel.webp "The eligibility row names each type that qualifies as-is; a list with a missing position or item drops out of it and appears under required properties instead.")

A parse error voids the block before any of this runs. A breadcrumb name that contains a double quote (`24" Monitor`) or a stray trailing comma makes the entire block, including any other entities in the same `@graph`, disappear for Google. The parse finding names the block number and the parser's message so you can find the character.

## The shape, line by line

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://www.example-shop.hr/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Shoes",
      "item": "https://www.example-shop.hr/shoes/"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Trail running",
      "item": "https://www.example-shop.hr/shoes/trail-running/"
    },
    {
      "@type": "ListItem",
      "position": 4,
      "name": "Red Runner"
    }
  ]
}
```

- **`itemListElement`** is the array of crumbs. It is the only property the list itself needs.
- **`position`** is a number starting at 1. Google uses it, not the array order, to sort the trail, so a gap (1, 2, 4) or a repeat (1, 2, 2) breaks the list. Write it as a number, not the string `"2"`.
- **`name`** is the text shown in the result. Keep it short; Google truncates long trails. It must match the visible crumb.
- **`item`** is the URL of that crumb's page. Use the absolute URL with the scheme and host: relative paths are resolved differently by different consumers, and the validator's URL comparison and Google's both expect a full URL. `item` may also be an object with `@id` and `name`, which is what Yoast prints; both forms are valid.
- **The last item** is the current page. Google's documentation says `item` is optional there, and it can be omitted. If you include it, it must be the page's canonical URL.

Whether the list starts with "Home" is a style choice: Google's examples begin at the first section, and either way works, as long as the visible trail begins at the same place.

## Step by step

### 1. Decide the one trail per page

Hierarchy, not history. A product reached from a sale page still lives in `Shoes › Trail running`. If the site has a primary category per item (WooCommerce and Yoast both have a "primary category" setting), the trail follows it. If a page genuinely belongs to two sections, it may carry two lists:

```json
[
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      { "@type": "ListItem", "position": 1, "name": "Shoes", "item": "https://www.example-shop.hr/shoes/" },
      { "@type": "ListItem", "position": 2, "name": "Red Runner" }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      { "@type": "ListItem", "position": 1, "name": "Sale", "item": "https://www.example-shop.hr/sale/" },
      { "@type": "ListItem", "position": 2, "name": "Red Runner" }
    ]
  }
]
```

Two lists is the maximum that makes sense; Google picks one to show. A list per possible path is noise.

### 2. Generate it from the same data as the visible trail

The two halves must agree, so they should come from one array. The [navigation guide](https://getreport.app/guides/breadcrumbs-navigation-and-structured-data) has a build-time JavaScript example that renders the `<nav>` and the JSON-LD from one list of crumbs. On a CMS, the plugin that prints the trail is the one that should print the list, for the same reason.

### 3. Escape names properly

Names come from page titles, and page titles contain quotes, ampersands and non-Latin characters. In JSON, a double quote inside a string must be `\"`, a backslash `\\`. Everything else, including `&`, `é` and `›`, is fine as-is. Use a JSON encoder, never string concatenation:

```php
<?php
// In a theme template, after building $crumbs as [['name' => …, 'url' => …], …]
$list = [
  '@context' => 'https://schema.org',
  '@type' => 'BreadcrumbList',
  'itemListElement' => [],
];
foreach ($crumbs as $i => $crumb) {
  $item = ['@type' => 'ListItem', 'position' => $i + 1, 'name' => $crumb['name']];
  if ($i < count($crumbs) - 1) {
    $item['item'] = $crumb['url'];
  }
  $list['itemListElement'][] = $item;
}
echo '<script type="application/ld+json">'
  . wp_json_encode($list, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
  . '</script>';
```

`wp_json_encode` (or plain `json_encode` outside WordPress) escapes the quote in `24" Monitor` for you and turns `</script>` inside a name into something harmless. The `JSON_UNESCAPED_SLASHES` flag keeps URLs readable; it is optional.

### 4. Put the list where the other structured data is

Anywhere in the HTML works; Google reads JSON-LD from `<head>` and `<body>` alike. If the page already has a `@graph` (Organization, WebSite, Product), the list can be one more node in it, with an `@id` so other nodes can reference it. [JSON-LD basics](https://getreport.app/guides/json-ld-basics) shows how graphs are assembled. One list per trail; do not repeat the list because the trail appears twice in the layout (top and footer).

### 5. Test in two places

Run the [schema validator](https://getreport.app/tools/schema-validator) for the parse and required-property checks, then Google's Rich Results Test for the eligibility as Google sees it. The Rich Results Test shows a "Breadcrumbs" item with the names it read; if a crumb is missing there, its `item` or `position` is the usual cause. Google's reference is the [breadcrumb structured data documentation](https://developers.google.com/search/docs/appearance/structured-data/breadcrumb).

## Platform notes

### WordPress

**Yoast SEO** adds a `BreadcrumbList` node to its graph on every page once breadcrumbs are enabled (Yoast SEO → Settings → Advanced → Breadcrumbs). It prints `item` as an object with `@id`, and omits it on the last crumb. The names come from the post title and the primary category; changing a category name changes the trail.

**Rank Math** does the same from Rank Math SEO → General Settings → Breadcrumbs; the `BreadcrumbList` is part of its graph whenever the module is on, whether or not the trail is placed in the template. That is the one case where the JSON-LD can exist without a visible trail: place the shortcode or the template call, or turn the setting off.

**WooCommerce** prints its own trail and a `BreadcrumbList` on shop, category and product pages. With Yoast or Rank Math also active, the page can carry two lists that say slightly different things (one includes "Shop", one does not). Let the SEO plugin replace WooCommerce's trail (Yoast has that option under its breadcrumb settings) or remove the WooCommerce hook in the child theme.

**Block themes** are the case to check carefully. As of WordPress 6.8 there is no core breadcrumbs block, so a trail in a block theme comes from the SEO plugin's block or from a pattern someone typed. A typed pattern is plain links: it adds no `BreadcrumbList`, and the validator will show the SEO audit finding passing (a class containing "breadcrumb") while the schema module finds no list. Use the Yoast or Rank Math block, which prints both halves.

### Shopify

Themes that print a breadcrumb snippet rarely add the JSON-LD. Check with the validator; if the eligibility finding does not name `BreadcrumbList`, add a Liquid block next to the snippet that builds the list from `collection` and `product`, using the `| json` filter to escape names:

```liquid
{% if collection and product %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": {{ collection.title | json }}, "item": {{ shop.url | append: collection.url | json }} },
    { "@type": "ListItem", "position": 2, "name": {{ product.title | json }} }
  ]
}
</script>
{% endif %}
```

### Static sites and custom code

Compute the ancestors from the URL path or from front matter at build time and render both halves from the array. Add a unit test that parses the generated JSON with a strict parser; the class of bug this catches (an unescaped quote in one product name out of 2,000) is invisible any other way.

## Verify

- The schema validator reads "All N JSON-LD blocks parse" and "Eligible for rich results: BreadcrumbList" (with your other types), with no `itemListElement[…]` path under required properties.
- The Rich Results Test shows one Breadcrumbs item per trail, with the same names as the visible bar, and no warnings.
- The SEO audit's breadcrumb finding lists both a `nav[aria-label]` signal and the JSON-LD signal, so the two halves exist.
- After Google recrawls (days to weeks), the listing for a deep page shows `example-shop.hr › Shoes › Trail running` instead of the URL. Search Console's Enhancements → Breadcrumbs report counts valid and invalid items across the site.

## Common mistakes

- **No `position`, or positions as strings.** Symptom: the Rich Results Test reports "Missing field position" or shows the crumbs in the wrong order. Add integer positions starting at 1.
- **Relative `item` URLs** (`/shoes/`). Symptom: the crumb is present but the trail links resolve to the wrong host in some tools, and the list is reported with an invalid URL. Use `https://www.example-shop.hr/shoes/`.
- **An `item` missing on a middle crumb.** The first crumb often has a name but no URL because the template forgot the home link. Every element except the last needs `item`.
- **Two lists that disagree.** WooCommerce's and the SEO plugin's, or the theme's and a plugin's. Keep one generator; the validator's duplicate finding and the Rich Results Test's item count show when there are two.
- **An unescaped quote in a name.** One product called `24" Monitor` voids the block on that page, and with it every other entity in the same `@graph`. Encode with a JSON library, and read the parse finding first.
- **Trail names that do not match the page.** "Trail Running Shoes" in the JSON-LD, "Trail running" on the page. Google compares them; generate both from one source.
