These Astro breadcrumbs take a list of { name, href } items and render two things from it: an accessible trail inside <nav aria-label="Breadcrumb"> with aria-current="page" on the last item, and matching BreadcrumbList structured data. A helper can also build the list automatically from the URL path, so most pages need no props at all.
Key Takeaways
- Use an ordered list inside
<nav aria-label="Breadcrumb">.- Mark the last item with
aria-current="page"and don’t link it.- Generate JSON-LD from the same items so markup and schema always agree.
- Item URLs in the schema must be absolute; build them from
Astro.site.
The component
---
// src/components/Breadcrumbs.astro
interface Crumb { name: string; href: string }
interface Props { items?: Crumb[] }
const titleCase = (s: string) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
// Fall back to building crumbs from the URL path
const segments = Astro.url.pathname.split('/').filter(Boolean);
const fromPath: Crumb[] = [
{ name: 'Home', href: '/' },
...segments.map((seg, i) => ({ name: titleCase(seg), href: `/${segments.slice(0, i + 1).join('/')}/` })),
];
const items = Astro.props.items ?? fromPath;
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, i) => ({
'@type': 'ListItem',
position: i + 1,
name: item.name,
item: new URL(item.href, Astro.site).href,
})),
};
---
<nav aria-label="Breadcrumb" class="breadcrumbs">
<ol>
{items.map((item, i) => (
<li>
{i < items.length - 1
? <a href={item.href}>{item.name}</a>
: <span aria-current="page">{item.name}</span>}
</li>
))}
</ol>
</nav>
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
<style>
.breadcrumbs ol { display: flex; flex-wrap: wrap; gap: 0.25rem; list-style: none; padding: 0; margin: 0; font-size: 0.9rem; }
.breadcrumbs li:not(:last-child)::after { content: '/'; margin-left: 0.25rem; opacity: 0.5; }
.breadcrumbs a { color: inherit; }
.breadcrumbs [aria-current='page'] { opacity: 0.7; }
</style>
The / separator is added in CSS, so screen readers don’t read it out.
Using it
Automatic, from the URL:
<Breadcrumbs />
On /blog/astro-routing-guide/ that gives Home / Blog / Astro Routing Guide. Path-derived names are only a fallback, so pass real titles where you have them:
---
const crumbs = [
{ name: 'Home', href: '/' },
{ name: 'Blog', href: '/blog/' },
{ name: post.data.title, href: `/blog/${post.id}/` },
];
---
<Breadcrumbs items={crumbs} />
Requirements
sitemust be set inastro.config.mjsfor absolute schema URLs. Without it,new URL(href, undefined)throws.- Use the same trailing-slash style as your canonical URLs; see trailingSlash and build.format.
- Every crumb except the last should be a page that exists. The path fallback assumes each folder has an index page; pass explicit items where that isn’t true.
Already have breadcrumbs in your layout?
Some layouts already output BreadcrumbList schema in the <head>. Check the page source before adding this component, so you don’t publish two conflicting breadcrumb lists.
The structured data pattern is explained in the Astro SEO checklist, and canonical URL rules in canonical URLs in Astro. For other reusable pieces, see the Astro components guide. Google’s breadcrumb structured data docs list the requirements.
FAQ
Should the current page be in the breadcrumb schema?
Including it is fine and common. In the visible trail, show it without a link.
Do breadcrumbs help SEO?
They help search engines understand site structure, and can replace the URL in search results. They also help visitors navigate.