BreadcrumbList schema in Next.js App Router needs no plugin. Here is a reusable TypeScript component that renders both the visible nav and the JSON-LD schema in one place.

href. URL mismatches between visible content and schema are the most common breadcrumb validation error.item property in the JSON-LD — it represents the current page, not a navigable link.Breadcrumb schema does one specific, measurable thing in search results: it replaces the raw URL in the SERP snippet with a readable path. Instead of https://seo.yatna.ai/seo-academy/breadcrumb-schema-next-js/, the search result shows seo.yatna.ai › SEO Academy › BreadcrumbList Schema. That single change consistently improves click-through rates for deep-directory pages where the raw URL is long and unreadable.
In Next.js App Router, implementing BreadcrumbList schema without a plugin takes about 30 minutes and results in a cleaner, more maintainable solution than any third-party package. This guide shows the complete implementation: the TypeScript component, correct JSON-LD format, usage patterns, and the errors that most commonly cause validation failures.
BreadcrumbList schema marks up the navigational path from your homepage to the current page. Google uses it in two ways:
1. SERP breadcrumb display: The breadcrumb trail appears below the page title in the search result, replacing the raw URL. This is a rich result — it requires valid BreadcrumbList schema to activate.
2. Site structure understanding: Google uses breadcrumb schema as a structural signal to understand your site hierarchy and content categorisation. A blog post at /seo-academy/breadcrumb-schema-next-js/ with a breadcrumb schema showing Home > SEO Academy > BreadcrumbList Schema tells Google this is a leaf-level page in the SEO Academy category.
For AI assistants, breadcrumb schema contributes to content categorisation — it helps the model understand where a piece of content sits in the site's topic hierarchy, which can influence how it categorises and cites the content.
Before the component, understand the schema structure. A three-level breadcrumb trail looks like this:
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://seo.yatna.ai/"
},
{
"@type": "ListItem",
"position": 2,
"name": "SEO Academy",
"item": "https://seo.yatna.ai/seo-academy/"
},
{
"@type": "ListItem",
"position": 3,
"name": "BreadcrumbList Schema in Next.js App Router"
}
]
}
Critical rules:
position starts at 1, not 0. Using 0-based indexing is invalid and will cause Rich Results Test failures.item. The final breadcrumb represents the current page. Including an item URL on the last element is technically allowed by Schema.org but Google recommends omitting it to clearly distinguish the current page from navigable links.item URLs must match your canonical URLs exactly, including trailing slashes. If your canonical is https://seo.yatna.ai/seo-academy/breadcrumb-schema-next-js/, the item URL in the schema must be exactly that — not without the trailing slash, not with query parameters.name must match the visible link text in the rendered breadcrumb navigation on the page.This component renders both the visible breadcrumb navigation and the JSON-LD schema block in one place. Co-locating them eliminates the most common source of schema errors: having the visible breadcrumb and the schema get out of sync.
// components/Breadcrumbs.tsx
interface BreadcrumbItem {
name: string
url: string
}
interface BreadcrumbsProps {
items: BreadcrumbItem[]
}
export function Breadcrumbs({ items }: BreadcrumbsProps) {
const schema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": items.map((item, index) => {
const isLast = index === items.length - 1
const listItem: Record<string, unknown> = {
"@type": "ListItem",
"position": index + 1,
"name": item.name,
}
// Omit 'item' property for the final breadcrumb (current page)
if (!isLast) {
listItem["item"] = item.url
}
return listItem
}),
}
return (
<>
<script
type="application/ld+json"
// schema is constructed from static application data, not user input
suppressHydrationWarning
// set innerHTML to JSON.stringify(schema) in your implementation
/>
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-2 text-sm text-gray-500">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<li key={item.url} className="flex items-center gap-2">
{index > 0 && (
<span aria-hidden="true" className="text-gray-300">
/
</span>
)}
{isLast ? (
<span aria-current="page" className="text-gray-900 font-medium">
{item.name}
</span>
) : (
<a
href={item.url}
className="hover:text-gray-900 transition-colors"
>
{item.name}
</a>
)}
</li>
)
})}
</ol>
</nav>
</>
)
}
Why this component structure:
items array. Change the items array and both update atomically.isLast logic: The component handles the "final item has no item property" rule automatically — you do not need to remember it at the call site.aria-label="Breadcrumb": Screen readers announce this as a breadcrumb navigation region. Required for accessibility and Google's Core Web Vitals accessibility scoring.aria-current="page": Marks the current page item for screen readers. Also signals to automated testing tools that the last item is the current page.// app/seo-academy/[slug]/page.tsx
import { Breadcrumbs } from '@/components/Breadcrumbs'
import { getPost } from '@/lib/blog'
export default async function PostPage({
params,
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
const breadcrumbs = [
{ name: 'Home', url: 'https://seo.yatna.ai/' },
{ name: 'SEO Academy', url: 'https://seo.yatna.ai/seo-academy/' },
{ name: post.title, url: `https://seo.yatna.ai/seo-academy/${params.slug}/` },
]
return (
<article>
<Breadcrumbs items={breadcrumbs} />
{/* rest of post content */}
</article>
)
}
// app/seo-academy/page.tsx
import { Breadcrumbs } from '@/components/Breadcrumbs'
export default function SEOAcademyPage() {
const breadcrumbs = [
{ name: 'Home', url: 'https://seo.yatna.ai/' },
{ name: 'SEO Academy', url: 'https://seo.yatna.ai/seo-academy/' },
]
return (
<main>
<Breadcrumbs items={breadcrumbs} />
{/* category page content */}
</main>
)
}
Two-level breadcrumbs on hub pages are correct. Do not force a three-level structure where only two levels exist — the schema should accurately represent the actual page hierarchy.
For large sites, define the base URL once to avoid hardcoding it across every breadcrumb call:
// lib/config.ts
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://seo.yatna.ai'
// app/seo-academy/[slug]/page.tsx
import { SITE_URL } from '@/lib/config'
const breadcrumbs = [
{ name: 'Home', url: `${SITE_URL}/` },
{ name: 'SEO Academy', url: `${SITE_URL}/seo-academy/` },
{ name: post.title, url: `${SITE_URL}/seo-academy/${params.slug}/` },
]
This ensures breadcrumb URLs in schema match your environment's canonical URL, which matters if you run preview deployments on different domains.
Symptom: Google Rich Results Test shows warning: "Breadcrumb item URL does not match link on page."
Cause: The item URL in the schema and the href on the visible <a> element differ. Typically a trailing slash mismatch: schema has /seo-academy/ but the anchor links to /seo-academy.
Fix: Ensure the url field in your BreadcrumbItem objects is used for both the schema's item field and the anchor's href. In the component above, item.url is used for both — this structural guarantee prevents the mismatch.
Symptom: Rich Results Test warning about position values.
Cause: Using 0-based indexing (index instead of index + 1) is the most common source. Sometimes items are numbered incorrectly when breadcrumbs are constructed manually rather than via a map.
Fix: Always use index + 1 in the position field. The component above handles this — do not override it at the call site.
Symptom: Rich Results Test error: "A field whose value should be present on the page is missing."
Cause: Adding BreadcrumbList schema to a page that has no visible breadcrumb navigation. Google requires that schema markup corresponds to visible page content.
Fix: Only render the <Breadcrumbs /> component (and therefore its schema) on pages where you want visible breadcrumb navigation. The homepage typically does not have breadcrumbs. Consider whether second-level pages (the hub pages) benefit from a two-item trail.
Symptom: No error in Rich Results Test, but SERP displays unexpected breadcrumb label.
Cause: The name field in the final breadcrumb item is different from the page's <title> or <h1>. Google may substitute its own label.
Fix: For the final breadcrumb item (the current page), use the same string as the page's H1 heading. For intermediate items, use the category or section name exactly as it appears in the navigation.
Symptom: Rich Results Test shows multiple BreadcrumbList entities on the same page.
Cause: BreadcrumbList schema injected in both a layout component (for all pages) and the page component itself.
Fix: Only render the <Breadcrumbs /> component in the page component, not in a layout. The root layout should contain Organisation schema (site-wide); BreadcrumbList schema is page-specific.
Step 1: Google Rich Results Test — paste your URL at search.google.com/test/rich-results. Look for "BreadcrumbList" in the detected items. Check for any errors (red) or warnings (orange).
Step 2: Google Search Console — after deployment, check the Enhancements section of Search Console for "Breadcrumbs" coverage. Pages with valid schema appear under "Valid" status; errors and warnings are listed with specific URLs and issue descriptions.
Step 3: Manual SERP check — search for your page title in Google. If BreadcrumbList schema is valid and Google has re-crawled the page, the breadcrumb trail should appear in the SERP result below the title. Note that SERP display may take days to weeks to update after schema changes.
Validate your schema markup against 70+ signals →
Does BreadcrumbList schema affect rankings directly?
Not directly. It is a rich result enhancement — it changes how your listing appears in the SERP, not where it ranks. The indirect effect on rankings comes through improved click-through rate: a breadcrumb trail makes the page's context immediately visible, which tends to increase CTR for pages where the URL path is not self-explanatory.
Should every page have breadcrumb schema?
Not necessarily. Breadcrumb schema is most valuable for deep pages — blog posts, documentation articles, product detail pages — where the hierarchical context adds meaningful information. The homepage and top-level category pages typically do not need it. Add it where visible breadcrumb navigation makes sense for the user experience.
Can I have multiple breadcrumb paths to the same page?
Schema.org allows multiple BreadcrumbList blocks representing different valid paths to a page. In practice, maintain one canonical breadcrumb path per page — the same path reflected in your site's navigation and internal links. Multiple paths add complexity without meaningful benefit for most sites.
Do I need a plugin like next-seo for breadcrumb schema in Next.js App Router?
No. The component in this guide is self-contained, type-safe, and requires no additional dependencies. Third-party packages add bundle weight and introduce a dependency layer with its own update cycle. The native approach is simpler and equally effective.
Validate your breadcrumb schema and all other structured data — run a free audit at seo.yatna.ai →
About the Author

Ishan Sharma
Head of SEO & AI Search Strategy
Ishan Sharma is Head of SEO & AI Search Strategy at seo.yatna.ai. With over 10 years of technical SEO experience across SaaS, e-commerce, and media brands, he specialises in schema markup, Core Web Vitals, and the emerging discipline of Generative Engine Optimisation (GEO). Ishan has audited over 2,000 websites and writes extensively about how structured data and AI readiness signals determine which sites get cited by ChatGPT, Perplexity, and Claude. He is a contributor to Search Engine Journal and speaks regularly at BrightonSEO.