On desktop, Google can replace the URL path under a search result with a readable trail such as example.com › Blog › Guides. BreadcrumbList markup is what feeds it. On phones, Google has shown only the domain since January 2025, so the visible effect is now a desktop one. Google's breadcrumb documentation also says the markup helps it "understand and categorize the information on the page".
Below: the rules Google actually publishes (they're looser than most guides suggest), a component for the Next.js App Router that keeps the markup and the visible trail in sync, and the handful of mistakes worth checking for.
Google's rules, as written
- At least two
ListItems. A one-item list isn't eligible. positionis an Integer, and 1 marks the start of the trail.- Follow a typical user path, not the URL. Google recommends "breadcrumbs that represent a typical user path to a page, instead of mirroring the URL structure."
- The homepage is optional, and so is the page itself. In Google's words: "It is not required to include a breadcrumb
ListItemfor the top level path (your site's domain or host name), nor for the page itself." itemis optional on the last breadcrumb. If it's missing, Google uses the URL of the page containing the markup.- Several trails are allowed when a page can be reached by more than one path.
The general structured data guidelines add one more: mark up content that users can see. In practice that means the JSON-LD and the breadcrumb on the page should describe the same trail.
Starting at Home and ending with the current page is a sensible default. It just isn't a requirement, and a trail that reflects how people browse your site (Blog › Guides › this post) is closer to what Google asks for than one that copies the folder structure.
The JSON
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://example.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Guides",
"item": "https://example.com/guides"
},
{
"@type": "ListItem",
"position": 3,
"name": "BreadcrumbList in Next.js"
}
]
}
The last item has no item URL, which Google accepts. Including it is also fine.
When a page genuinely sits in two places, publish two trails as an array in one script tag. Google's own documentation shows this pattern:
[
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Guides", "item": "https://example.com/guides" },
{ "@type": "ListItem", "position": 2, "name": "Structured data", "item": "https://example.com/guides/structured-data" },
{ "@type": "ListItem", "position": 3, "name": "BreadcrumbList in Next.js" }
]
},
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Tutorials", "item": "https://example.com/tutorials" },
{ "@type": "ListItem", "position": 2, "name": "Next.js", "item": "https://example.com/tutorials/nextjs" },
{ "@type": "ListItem", "position": 3, "name": "BreadcrumbList in Next.js" }
]
}
]
A BreadcrumbList component for the App Router
The approach: one array of crumbs per page, used to render both the visible <nav> and the JSON-LD. If they come from the same data, they can't drift apart.
// components/Breadcrumbs.tsx (a Server Component)
import Link from "next/link";
const SITE_URL = "https://example.com";
export type Crumb = { name: string; path?: string };
export function Breadcrumbs({ crumbs }: { crumbs: Crumb[] }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: crumbs.map((crumb, i) => ({
"@type": "ListItem",
position: i + 1,
name: crumb.name,
...(crumb.path ? { item: new URL(crumb.path, SITE_URL).toString() } : {}),
})),
};
return (
<>
{crumbs.length >= 2 && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
)}
<nav aria-label="Breadcrumb">
<ol>
{crumbs.map((crumb) => (
<li key={crumb.name}>
{crumb.path ? (
<Link href={crumb.path}>{crumb.name}</Link>
) : (
<span aria-current="page">{crumb.name}</span>
)}
</li>
))}
</ol>
</nav>
</>
);
}
Then in the route:
// app/guides/[slug]/page.tsx
import { Breadcrumbs } from "@/components/Breadcrumbs";
import { getGuide } from "@/lib/guides";
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const guide = await getGuide(slug);
return (
<article>
<Breadcrumbs
crumbs={[
{ name: "Home", path: "/" },
{ name: "Guides", path: "/guides" },
{ name: guide.title },
]}
/>
<h1>{guide.title}</h1>
{/* … */}
</article>
);
}
What each part is doing:
position: i + 1. Array indexes start at 0 and breadcrumb positions start at 1. This is where most numbering bugs come from.new URL(crumb.path, SITE_URL)turns relative paths into the absolute URLs the markup needs, while the<Link>keeps using relative ones.- The
.replace(/</g, "\\u003c").JSON.stringifydoesn't escape</script>, so a title containing it would close the script tag early. The Next.js JSON-LD guide recommends exactly this replacement; the output is still valid JSON. - The
crumbs.length >= 2guard. Google needs two items, so a single crumb renders the nav but no markup. - No
itemon the last crumb. It matches the visible trail, where the current page isn't a link. - It's a Server Component. The script is in the HTML the server sends. Google can process JSON-LD injected by JavaScript, but Vercel's crawler study found GPTBot, ClaudeBot and PerplexityBot don't run JavaScript, so building it in a
useEffecthides it from them.
Use labels written for people. Deriving names from slugs gives you trails like "aeo-tools"; a title or label field in your content source avoids that. If a page lives in two sections, render two Breadcrumbs instances or extend the component to accept several trails and output the array shown earlier.
WordPress, Webflow and headless setups
On WordPress, Yoast and Rank Math both include a BreadcrumbList in their schema output once breadcrumbs are enabled. Run only one of them, or you can end up with the same trail twice. On Webflow or a headless CMS, the pattern is the one above: build the trail on the server from CMS fields rather than in client-side script.
Mistakes worth checking for
- Markup and visible breadcrumb disagree. The JSON-LD says Home › Blog › Post while the page shows Home › Post. Generate both from the same data.
- The same trail twice. Two plugins, or a plugin plus a theme, each emitting an identical BreadcrumbList is noise; remove one. Intentional, different trails are fine.
- A one-item list. Not eligible. Two items is the minimum.
- Positions starting at 0 or written as strings. Use integers from 1.
- URLs in the trail that 404. They send users and crawlers to dead pages; check them after any URL restructure.
- Markup that exists only after hydration. Check view-source, not the browser inspector.
Checking that it works
Run the live URL through Google's Rich Results Test. Using the URL rather than pasted code means Google fetches and renders the page; "Breadcrumbs" should appear as a detected item with no errors. After Google has recrawled the page, Search Console's Breadcrumbs report (under Enhancements) shows valid and invalid items across the site.
To see the result in search, look on desktop. A phone will show only the domain, which is expected since January 2025, not a sign the markup failed.
As for AI search: Google's guide to generative AI features says there's "no special schema.org markup you need to add". BreadcrumbList is for the desktop result and for clear site structure. Nobody has shown it affects AI citations.
Next step: test one template
Breadcrumbs come from templates, so one check covers every page that uses the template. Paste a URL from each template type (post, category, product) into the tool below, fix what it reports, then re-test one URL in the Rich Results Test.
This blog generates its own trail the same way (Home › Blog › post title) from one function. The other markup that belongs on every post is the author, covered in Article schema with author.
FAQ
Can I add BreadcrumbList if the page shows no breadcrumb?
Can BreadcrumbList markup hurt rankings?
Sources cited in this piece
- Google Search Central: Breadcrumb structured data (desktop availability, user-path recommendation, optional top level and last
item, multiple trails, two-item minimum) - Google Search Central blog: Simplifying the visible URL element on mobile search results (January 2025)
- Google Search Central: General structured data guidelines
- Google Search Central: Generate structured data with JavaScript
- Google Search Central: Optimizing for generative AI features on Search (May 2026)
- Next.js: How to implement JSON-LD in your Next.js application
- Vercel: The rise of the AI crawler
- Schema.org: BreadcrumbList
Updated October 1, 2026: rewritten for Google's current rules (desktop-only display since January 2025, optional homepage and last URL, multiple trails allowed) and refocused on a Next.js App Router implementation.