Below the title in a search result, Google shows either a raw URL or a clean path like example.com › Tools › Image Converter. The path version comes from breadcrumb schema. It's one of the smallest implementation efforts in structured data and one of the most consistently effective — it works for almost any site, not just specific content types.
This is also where a lot of developers get tripped up on small things. The position field needs to be sequential with no gaps. Every URL in item needs to resolve. Minimum two items — a single-item breadcrumb is invalid per spec. These aren't complicated rules, but they're the ones that silently break implementations.
What's covered: the correct structure for shallow and deep hierarchies, how to generate breadcrumbs dynamically from URL paths, combining breadcrumb schema with other schema types on the same page, and the six mistakes that show up most often in validation reports.
What Is Breadcrumb Schema?
BreadcrumbList is a schema.org type that describes a page's position in a site hierarchy — the navigation path from the homepage to the current page. You implement it as JSON-LD and Google uses it to replace the URL display in search results with a human-readable path.
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Tools",
"item": "https://example.com/tools"
},
{
"@type": "ListItem",
"position": 3,
"name": "Image Converter",
"item": "https://example.com/tools/image-converter"
}
]
}
This produces example.com › Tools › Image Converter in search results instead of the raw URL.
What Breadcrumb Schema Looks Like in Search Results
The breadcrumb trail replaces the green URL text that appears below the page title in standard search results. Before breadcrumb schema (or when it's not detected), Google shows the URL — often long, often ugly, often including query parameters. With it, you get a clean hierarchical path.
Before: https://example.com/en/tools/image/converter?format=webp&quality=85
After: example.com › Tools › Image Converter
The improvement has two practical effects:
Readability. Users understand immediately where this page lives in your site's structure. A page that says Tools › Image Converter communicates category context before the user clicks.
CTR. Cleaner, shorter path displays often improve click-through rates, particularly on mobile where long URLs truncate awkwardly. The breadcrumb display is also slightly more compact, giving more visible space to the title and description.
When Google Shows Breadcrumbs in Search Results
Google uses two sources to generate breadcrumb trails in search results:
- Breadcrumb schema — explicit structured data you provide
- URL structure — Google's own parsing of your URL path as a fallback
If you implement breadcrumb schema correctly, Google uses your data and shows it as you've defined it. If you don't, Google attempts to construct a breadcrumb from your URL structure — with varying accuracy.
The practical implication: even if your URL structure is clean (/tools/image-converter), you should still implement breadcrumb schema. The schema version gives you precise control over:
- What names appear at each level
- Which URLs each crumb points to
- How many levels to show
URL-derived breadcrumbs sometimes produce odd results with dynamic routes, query parameters, or flat URL structures. Schema removes that uncertainty.
The Minimum Valid Breadcrumb Schema
Google requires at least two ListItem objects in a BreadcrumbList. A single-item list is invalid — breadcrumbs exist to show hierarchy, and hierarchy requires at least two levels. Each item must have position and name. The item (URL) is technically optional for the last item — since it represents the current page, Google infers it from the crawled URL.
For a two-level hierarchy (home → page):
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "About",
"item": "https://example.com/about"
}
]
}
For most tools and blog pages this is all you need. The current page is the last item. The homepage is always position 1.
Breadcrumb Schema for Deep Hierarchies
E-commerce sites, documentation sites, and blogs with categories often have three or four levels of hierarchy. Each level gets its own ListItem:
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://shop.example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Electronics",
"item": "https://shop.example.com/electronics"
},
{
"@type": "ListItem",
"position": 3,
"name": "Cameras",
"item": "https://shop.example.com/electronics/cameras"
},
{
"@type": "ListItem",
"position": 4,
"name": "Sony A7C II",
"item": "https://shop.example.com/electronics/cameras/sony-a7c-ii"
}
]
}
In search results: shop.example.com › Electronics › Cameras › Sony A7C II
Rules for deep hierarchies:
-
positionvalues must be sequential integers starting at 1 — no gaps, no duplicates - Each intermediate level should have a real, accessible URL in
item - The final item represents the current page — its
itemshould match the canonical URL of the page
Field Reference
| Field | Type | Required | Notes |
|---|---|---|---|
@type |
Text | Yes | Must be "BreadcrumbList"
|
itemListElement |
Array | Yes | Array of ListItem objects |
ListItem.@type |
Text | Yes | Must be "ListItem"
|
ListItem.position |
Integer | Yes | Sequential starting at 1. No gaps. |
ListItem.name |
Text | Yes | Human-readable label for this level |
ListItem.item |
URL | Conditional | Required for all items except the last. Recommended for the last item too. |
That's the complete field set. Breadcrumb schema is deliberately simple — the value is in correct structure and real URLs, not in field variety.
The item Field: Should the Last Crumb Have a URL?
Technically, Google says item is not required for the last list item — since the last crumb represents the current page, Google can infer the URL from the page being crawled.
In practice: always include item for every crumb, including the last one.
Reasons:
- Explicit is always safer than inferred
- Some validators throw warnings when the last item has no
itemURL - It guarantees the correct canonical URL is associated with the last crumb, which matters for pages with URL parameters or multiple access paths
The last item's item URL should exactly match the canonical URL of the page — the same URL in your <link rel="canonical"> tag.
Matching Breadcrumb Schema to Visible Breadcrumb HTML
Google requires your breadcrumb structured data to correspond to an actual breadcrumb trail visible on the page. This is the most important rule and the most commonly violated one.
If your page has a visible breadcrumb navigation:
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/tools">Tools</a></li>
<li aria-current="page">Image Converter</li>
</ol>
</nav>
Your JSON-LD must mirror this exactly. Same names, same URLs, same order.
If there's no visible breadcrumb on the page at all — Google may still process the schema, but the risk of a quality flag increases. The best implementation pairs HTML breadcrumb navigation (good for users) with JSON-LD breadcrumb schema (good for machines). They reference the same data.
Combining BreadcrumbList with Other Schema Types
A page can have both BreadcrumbList and another primary schema type. This is common and recommended:
<!-- Primary schema: what the page is about -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Image Converter",
"url": "https://devtools.abect.com/png-to-jpg"
}
</script>
<!-- Navigation schema: where the page lives in the site -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://devtools.abect.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Image Tools",
"item": "https://devtools.abect.com/image-tools"
},
{
"@type": "ListItem",
"position": 3,
"name": "PNG to JPG",
"item": "https://devtools.abect.com/png-to-jpg"
}
]
}
</script>
Two separate <script> blocks — one for the primary content type, one for navigation. Don't merge them into a single object.
Alternative: Embedding BreadcrumbList Inside WebPage Schema
Some implementations nest the breadcrumb inside a WebPage schema using the breadcrumb property:
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "PNG to JPG Converter",
"url": "https://example.com/png-to-jpg",
"breadcrumb": {
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "PNG to JPG",
"item": "https://example.com/png-to-jpg"
}
]
}
}
This is valid schema.org syntax. Google supports both the standalone BreadcrumbList and the nested breadcrumb property approach. Use whichever fits your existing schema architecture — if you already have WebPage schema, nesting it is clean. If you don't, use a standalone block.
Implementation in Next.js (App Router)
For a tool site where every tool lives at a flat URL like /tool-name:
// components/BreadcrumbSchema.tsx
type BreadcrumbItem = {
name: string
url: string
}
export function BreadcrumbSchema({ items }: { items: BreadcrumbItem[] }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
)
}
Usage in a tool page:
// app/png-to-jpg/page.tsx
import { BreadcrumbSchema } from '@/components/BreadcrumbSchema'
export default function PngToJpgPage() {
return (
<>
<BreadcrumbSchema
items={[
{ name: 'Home', url: 'https://devtools.abect.com' },
{ name: 'PNG to JPG', url: 'https://devtools.abect.com/png-to-jpg' },
]}
/>
{/* page content */}
</>
)
}
For category-based sites, pass the category as the middle item:
<BreadcrumbSchema
items={[
{ name: 'Home', url: 'https://example.com' },
{ name: 'Electronics', url: 'https://example.com/electronics' },
{ name: 'Cameras', url: 'https://example.com/electronics/cameras' },
{ name: 'Sony A7C II', url: 'https://example.com/electronics/cameras/sony-a7c-ii' },
]}
/>
The component handles position automatically via the array index — one less thing to manually maintain.
Dynamic Breadcrumbs from URL Path
For sites where the URL structure directly maps to the breadcrumb hierarchy (clean, no vanity URLs), you can generate the breadcrumb schema dynamically from the current path:
// lib/breadcrumbs.ts
const LABELS: Record<string, string> = {
'': 'Home',
'tools': 'Tools',
'image': 'Image Tools',
'seo': 'SEO Tools',
'png-to-jpg': 'PNG to JPG',
'compress-jpg': 'Compress JPG',
'meta-tags-generator': 'Meta Tags Generator',
}
export function buildBreadcrumbsFromPath(pathname: string, baseUrl: string) {
const segments = pathname.split('/').filter(Boolean)
const items = [{ name: 'Home', url: baseUrl }]
let accumulatedPath = baseUrl
for (const segment of segments) {
accumulatedPath = `${accumulatedPath}/${segment}`
items.push({
name: LABELS[segment] ?? segment.replace(/-/g, ' '),
url: accumulatedPath,
})
}
return items
}
Usage:
import { usePathname } from 'next/navigation'
import { buildBreadcrumbsFromPath } from '@/lib/breadcrumbs'
import { BreadcrumbSchema } from '@/components/BreadcrumbSchema'
export default function Page() {
const pathname = usePathname()
const breadcrumbs = buildBreadcrumbsFromPath(pathname, 'https://devtools.abect.com')
return (
<>
<BreadcrumbSchema items={breadcrumbs} />
{/* content */}
</>
)
}
The LABELS map handles the display name for each segment — because png-to-jpg should display as "PNG to JPG", not "png to jpg". Segments without a defined label fall back to replacing hyphens with spaces.
Implementation in React (with react-helmet-async)
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
function BreadcrumbSchema({ crumbs }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: crumbs.map((crumb, i) => ({
'@type': 'ListItem',
position: i + 1,
name: crumb.name,
item: crumb.url,
})),
}
return (
<Helmet>
<script type="application/ld+json">
{JSON.stringify(schema)}
</script>
</Helmet>
)
}
// Usage:
function ToolPage() {
return (
<>
<BreadcrumbSchema
crumbs={[
{ name: 'Home', url: 'https://example.com' },
{ name: 'Tools', url: 'https://example.com/tools' },
{ name: 'Image Converter', url: 'https://example.com/tools/image-converter' },
]}
/>
{/* tool UI */}
</>
)
}
Implementation in WordPress
WordPress has a built-in breadcrumb trail concept via plugins (Yoast, RankMath) or theme functions. For manual implementation without plugins:
function add_breadcrumb_schema() {
// Build breadcrumbs based on current page type
$items = [];
$position = 1;
$base_url = home_url( '/' );
// Always start with home
$items[] = [
'@type' => 'ListItem',
'position' => $position++,
'name' => 'Home',
'item' => $base_url,
];
if ( is_category() || is_single() ) {
$categories = get_the_category();
if ( $categories ) {
$cat = $categories[0];
$items[] = [
'@type' => 'ListItem',
'position' => $position++,
'name' => $cat->name,
'item' => get_category_link( $cat->term_id ),
];
}
}
if ( is_single() || is_page() ) {
$items[] = [
'@type' => 'ListItem',
'position' => $position,
'name' => get_the_title(),
'item' => get_permalink(),
];
}
if ( count( $items ) < 2 ) return; // Don't output single-item breadcrumbs
$schema = [
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $items,
];
echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>';
}
add_action( 'wp_head', 'add_breadcrumb_schema' );
For WooCommerce product pages, add a check for is_product() and use wc_get_breadcrumb() to get the WooCommerce breadcrumb data, then map it to the schema structure.
The 6 Most Common Breadcrumb Schema Mistakes
1. position values with gaps or duplicates
// Wrong — gap at 2, duplicate at 3
[
{ "position": 1, "name": "Home" },
{ "position": 3, "name": "Tools" },
{ "position": 3, "name": "Image Converter" }
]
// Correct — sequential, no gaps
[
{ "position": 1, "name": "Home" },
{ "position": 2, "name": "Tools" },
{ "position": 3, "name": "Image Converter" }
]
Google's validator flags non-sequential positions as errors. The position values must be consecutive integers starting at 1.
2. item URLs that don't match actual pages
Every URL in item must resolve to a real, accessible page. If /tools returns 404 but you include it as a breadcrumb item, Google will flag it. Only include levels that have real pages.
3. Breadcrumb schema that doesn't match the visible breadcrumb
If the page shows "Home > Tools > Converter" but the schema says "Home > Image > PNG Converter", it's a mismatch. Google cross-validates structured data against visible content. Keep them in sync — ideally by generating both from the same data source.
4. Using a single-item breadcrumb
A BreadcrumbList with only one item — just the current page — is pointless and may trigger a warning. Breadcrumbs are about hierarchy. If a page is the homepage, it doesn't need a breadcrumb. If it's one level deep, include at minimum Home + current page (two items).
5. Non-canonical item URLs
The item URLs must match the canonical URLs of those pages — the same URLs in their <link rel="canonical"> tags. Don't use trailing-slash variants, www vs non-www variants, or http vs https variants that differ from canonical. Inconsistency weakens the schema's value.
6. Putting @type: BreadcrumbList in the wrong place
A BreadcrumbList should not be nested inside properties of other schema types (except the breadcrumb property of WebPage). It should be a top-level JSON-LD object or nested specifically under WebPage.breadcrumb. Nesting it inside, say, a Product object is invalid.
Breadcrumb Best Practices for SEO
Beyond the structural requirements, a few content decisions affect how useful your breadcrumbs are:
Use meaningful names, not URLs. The name field is what users see in search results. "Image Tools" reads better than "image-tools" or "Image_Tools". Capitalize correctly and use the human-readable label, not the slug.
Reflect your actual URL structure. Your breadcrumb schema should mirror your URL hierarchy. If your URL is /tools/image/png-to-jpg, the breadcrumb should be Home › Tools › Image › PNG to JPG. Don't fabricate a hierarchy that doesn't exist in your URL structure — it confuses both users and crawlers.
Keep depth reasonable. Google will display breadcrumbs in search results, but very deep hierarchies (6+ levels) may be truncated. More than 4 levels is rare for most sites. If you find yourself at 5+ levels, consider whether your site architecture needs simplification.
Include breadcrumbs on every non-homepage page. Homepage doesn't need a breadcrumb. Every other page benefits from one — it's a small implementation cost with consistent SEO upside.
Validating Breadcrumb Schema
Google Rich Results Test — search.google.com/test/rich-results
Paste the page URL or raw JSON-LD. The test shows whether your BreadcrumbList is detected and lists any errors. Common errors include non-sequential positions, missing name fields, and unreachable item URLs.
Schema.org Validator — validator.schema.org
Validates the structural correctness of your JSON-LD. Use it to catch type errors that Google's tool sometimes misses.
Live search preview: After deploying, search your site by name in Google and check whether the URL display has changed to the breadcrumb format. This can take anywhere from a few days to a few weeks after Google re-crawls the page.
Google Search Console → Enhancements → Breadcrumbs
Shows real crawl data across your entire site. If breadcrumb schema is deployed at scale (e-commerce, blog), this report surfaces structural errors and which URLs are affected.
Wrapping Up
Breadcrumb schema is probably the least exciting thing to write about in structured data, but it's also the one with the most predictable payoff. Small field set, works on almost any site, you can see the effect in search results within days of the page being re-crawled. The things that break it are mechanical — sequential positions, real URLs, minimum two items — and they're all easily caught with the Rich Results Test before you deploy.
Before shipping:
-
positionvalues start at 1 and increment by 1, no exceptions - Every URL in
itemmust return a real page — 404s silently invalidate the crumb - Last item's URL must match the page's canonical URL exactly
- If you have visible breadcrumb navigation on the page, the JSON-LD must match it
Generate breadcrumbs from a reusable component that shares its data with both the HTML navigation and the JSON-LD block. That way they're always in sync and you only update one place.
If you'd rather generate the BreadcrumbList JSON through a form: Breadcrumb Schema Generator. Free, no backend, runs in the browser.
Working with a site that has a flat URL structure with no real category hierarchy? Drop the setup in the comments — the "one level deep" breadcrumb case is more nuanced than it looks.
Top comments (0)