Skip to content
ExponaBLOG
TECHNICAL SEO FOR DEVELOPERS · DAY 033 / 100

BreadcrumbList structured data: best practices explained

Turn ugly URLs into readable paths in search results.

Expona Admin7 min read

Understanding BreadcrumbList structured data

Search engines show breadcrumb trails in SERPs to help users understand page hierarchy and navigate faster. Without structured data markup, Google can't reliably extract your breadcrumb information, leaving search results less informative. BreadcrumbList structured data: best practices explained is about encoding your navigation paths as JSON-LD so search engines and browsers render them consistently across devices.

We've all landed on a search result where the snippet showed a jumbled URL instead of a clean hierarchy like "Home > Products > Electronics > Laptops." That's because the site either omitted breadcrumb markup or structured it incorrectly. When you implement this properly, Google displays your breadcrumbs in the SERP, reducing bounce rates and improving click-through rates on long-tail queries.

One ListItem per level

Every step in your breadcrumb hierarchy must be a separate ListItem object in your JSON-LD array. This isn't optional; Google's structured data validator expects discrete items, each with its own position and URL.

Consider an e-commerce site selling kitchen equipment. A product page for a stainless steel chef's knife lives at /categories/cookware/knives/chefs-knife. Your breadcrumb trail has four levels:

  1. Home
  2. Cookware
  3. Knives
  4. Chef's Knife

Each level becomes a ListItem. If you skip a level or combine two into one ListItem, Google may misinterpret your hierarchy or ignore the markup entirely. The position field then tells Google which step each ListItem occupies in the sequence.

html
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://example.com"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Cookware",
      "item": "https://example.com/categories/cookware"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Knives",
      "item": "https://example.com/categories/cookware/knives"
    },
    {
      "@type": "ListItem",
      "position": 4,
      "name": "Chef's Knife",
      "item": "https://example.com/categories/cookware/knives/chefs-knife"
    }
  ]
}

This structure is unambiguous. Google knows you have exactly four levels, in that order, and can render them reliably in search results.

Position, name and item URL

Each ListItem requires three core fields: position (an integer starting at 1), name (the human-readable label), and item (the absolute URL). These fields work together to form the complete picture of your breadcrumb.

Position is a sequential counter. Position 1 is always the top level, position 2 the next, and so on. Don't skip numbers; if you have five levels, use 1, 2, 3, 4, 5. Google uses position to render breadcrumbs in the correct order, especially when crawling or indexing multiple language versions or regional variants of the same page.

Name should match what appears on the page. If your visible breadcrumb says "Kitchen Knives," your JSON-LD name field must also say "Kitchen Knives." Discrepancies confuse users and can trigger manual actions if Google suspects manipulation. Keep names concise—aim for two to five words per level.

Item must be a full, absolute URL (starting with https://) pointing to that breadcrumb's page. Google crawls these links to verify breadcrumb accuracy and to understand your site structure. Relative URLs or malformed paths won't work. Always use your canonical domain to avoid duplicate content issues.

html
{
  "@type": "ListItem",
  "position": 2,
  "name": "Cookware",
  "item": "https://example.com/categories/cookware"
}

In a SaaS product like a CRM, the same logic applies. A support page at /help/articles/managing-contacts might have a breadcrumb path: Home > Help > Articles > Managing Contacts. Each step gets its own ListItem with position, name, and item URL.

Match the visible breadcrumbs

The cardinal rule: your JSON-LD breadcrumb markup must reflect what users actually see on the page. If the visible breadcrumb displays "Home > Blog > SEO Tips" but your JSON-LD shows "Home > Articles > Search Engine Optimization," you've created a mismatch.

Why does this matter? Users read the visible breadcrumb and decide whether to click. If they click and land on the page they expected, they stay. If the JSON-LD doesn't match, Google might show the wrong breadcrumb in the SERP, and users land confused. This increases bounce rate and signals poor experience.

Mismatches also trigger quality issues in Search Console. If Google's crawler detects that your structured data doesn't align with the page content, it may deprioritize the breadcrumb rich result or ignore your markup entirely.

To avoid this, generate your JSON-LD from the same source as your visible breadcrumb. In a React app, for example:

html
const breadcrumbItems = [
  { name: "Home", url: "/" },
  { name: "Blog", url: "/blog" },
  { name: "SEO Tips", url: "/blog/seo-tips" }
];

// Render visible breadcrumb
const VisibleBreadcrumb = () => (
  <nav>
    {breadcrumbItems.map((item, index) => (
      <a key={index} href={item.url}>{item.name}</a>
    ))}
  </nav>
);

// Generate JSON-LD from same array
const BreadcrumbJSON = () => {
  const schema = {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": breadcrumbItems.map((item, index) => ({
      "@type": "ListItem",
      "position": index + 1,
      "name": item.name,
      "item": `${process.env.NEXT_PUBLIC_DOMAIN}${item.url}`
    }))
  };
  return <script type="application/ld+json">{JSON.stringify(schema)}</script>;
};

By sourcing both from the same array, you guarantee they stay in sync. When your content team updates a breadcrumb label, both the visible and structured versions update together.

Generate from your route hierarchy

The most maintainable approach is to derive breadcrumbs from your application's routing structure. Most modern frameworks—Next.js, React Router, NestJS, and others—already know your URL patterns.

In a Next.js app using the App Router, you might organize pages like this:

html
app/
├── page.tsx (Home)
├── blog/
│   ├── page.tsx (Blog index)
│   └── [slug]/
│       └── page.tsx (Individual blog post)
└── docs/
    ├── page.tsx (Docs index)
    └── [category]/
        └── [article]/
            └── page.tsx (Individual doc)

Your breadcrumb for a blog post at /blog/typescript-best-practices should automatically be: Home > Blog > TypeScript Best Practices. Generate it once from the route structure, and it works everywhere.

Here's a utility function that parses a pathname into breadcrumb segments:

html
function generateBreadcrumbs(pathname: string) {
  const segments = pathname.split("/").filter(Boolean);
  const breadcrumbs = [
    { name: "Home", url: "/" }
  ];

  let currentPath = "";
  segments.forEach((segment, index) => {
    currentPath += `/${segment}`;
    const label = segment
      .replace(/-/g, " ")
      .replace(/\b\w/g, (char) => char.toUpperCase());
    breadcrumbs.push({
      name: label,
      url: currentPath
    });
  });

  return breadcrumbs;
}

// For /blog/typescript-best-practices:
// Returns [
//   { name: "Home", url: "/" },
//   { name: "Blog", url: "/blog" },
//   { name: "Typescript Best Practices", url: "/blog/typescript-best-practices" }
// ]

This approach scales. Add a new route and breadcrumbs work automatically. No manual updates, no mismatches.

For dynamic routes with slugs, you'll need a lookup table or database query to convert slugs to readable names. A blog post slug seo-tips should become "SEO Tips," not "Seo Tips." Store a mapping or fetch the actual page title from your CMS.

Common mistakes

  • Skipping the Home level. Many teams omit the homepage from their breadcrumb schema. Google typically expects it as position 1. Include it.
  • Using relative URLs. Structured data requires absolute URLs. /blog won't work; use https://example.com/blog.
  • Mismatching position numbers. Don't use 1, 2, 4, 5 (skipping 3). Always use consecutive integers starting at 1.
  • Hardcoding breadcrumbs. If you hardcode JSON-LD for every page, you'll create inconsistencies when URLs change. Generate from your route structure instead.
  • Not validating in Search Console. Use Google's Rich Results Test to verify your breadcrumb markup before launch. It catches syntax errors and mismatches.
  • Breadcrumbs longer than seven levels. Very deep hierarchies confuse users and clutter SERPs. Aim for three to five levels. If you have more, consider restructuring your information architecture.

Key takeaways

  • Encode each breadcrumb level as a separate ListItem with position, name, and item URL.
  • Match your JSON-LD breadcrumb markup exactly to what users see on the page.
  • Always use absolute URLs (with the full domain) in your item fields.
  • Generate breadcrumbs from your application's routing structure to eliminate manual errors and keep them in sync.
  • Validate your breadcrumb schema using Google's Rich Results Test before pushing to production.
  • Keep breadcrumbs concise and avoid going deeper than seven levels.
  • Test across multiple pages and routes to catch inconsistencies early.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles