Breadcrumbs
Contribute one localized crumb per plugin route and let VitNode assemble the trail for public pages and AdminCP screens.
VitNode renders breadcrumb trails automatically in both the public site header and the AdminCP shell. A route joins the trail by declaring a crumb, and the trail reads parent to child:
Home / Notes / Getting StartedA route declares only what its own crumb should display. VitNode manages the rest: accessible <nav> markup, separators, locale-aware links, and aria-current="page" semantics. No manual link construction, no duplicated trails, and zero drama.
Static crumb
Declare a breadcrumb function directly inside definePluginRoute. Because it renders within the route's declared namespaces, useTranslations works out of the box:
import { definePluginRoute } from '@vitnode/core/routing'
import { useTranslations } from 'use-intl'
export const route = definePluginRoute({
breadcrumb: () => {
const t = useTranslations('@acme/site-notes.home')
return t('title')
},
})
const NotesLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="container mx-auto flex max-w-4xl flex-col gap-6 p-4">
<header className="border-b pb-4">
<h1 className="text-2xl font-bold tracking-tight text-balance">
Notes
</h1>
</header>
<main>{children}</main>
</div>
)
}
export default NotesLayoutDynamic crumb from loader
When a route is dynamic (such as /notes/:slug), its breadcrumb can read directly from the route's loaderData. This ensures the trail shows human-readable titles instead of raw IDs, without firing a second network request:
import {
definePluginRoute,
type PluginRoutePageProps,
} from '@vitnode/core/routing'
import { useTranslations } from 'use-intl'
interface Note {
content: string
title: string
}
export const route = definePluginRoute({
load: async ({ params }): Promise<Note> => {
return {
content: 'Loaded securely from your plugin loader.',
title: `Note: ${params.slug}`,
}
},
head: ({ loaderData, params }) => ({
title: loaderData?.title ?? params.slug,
}),
breadcrumb: ({ loaderData }) => {
const t = useTranslations('@acme/site-notes.home')
return `${t('note')}: ${loaderData.title}`
},
})
const NotePage = ({ loaderData }: PluginRoutePageProps<Note>) => {
return (
<article className="container mx-auto flex max-w-3xl flex-col gap-4 p-4">
<h2 className="text-3xl font-semibold tracking-tight text-balance">
{loaderData.title}
</h2>
<p className="text-muted-foreground leading-relaxed text-pretty">
{loaderData.content}
</p>
</article>
)
}
export default NotePageThe inline breadcrumb receives { loaderData, params, search }—the exact same typed context passed to head and the page component.
Omitting a route
Silence is the default: a route that declares no breadcrumb adds none, and the crumbs above it stay intact. Set breadcrumb: false when you want that silence to be deliberate—an index() page whose parent layout already names the screen, say:
import { definePluginRoute } from '@vitnode/core/routing'
export const route = definePluginRoute({
breadcrumb: false,
})
const NotesIndexPage = () => {
return (
<div>
<p className="text-muted-foreground leading-relaxed text-pretty">
Welcome to Site Notes.
</p>
</div>
)
}
export default NotesIndexPageBoth spellings do the same thing. The trail simply ends at the deepest route that does declare a crumb, and that crumb is the one marked aria-current="page".
Trail behavior
| Declaration | Behavior in breadcrumb trail |
|---|---|
breadcrumb: Component | Contributes one crumb with access to loaderData, params, and search |
breadcrumb: false | Omitted from trail; parent crumbs remain visible |
Omitted / undefined | The default: omitted from trail |
| Intermediate crumbs | Automatically rendered as locale-aware links to their respective route URLs |
| Leaf crumb (last item) | Rendered as text without a link and marked with aria-current="page" |
Return text or inline elements, not a trail
A breadcrumb component should return text or an inline element (like an icon with text). Never render <Breadcrumb>, <nav>, links, or separators—the shell creates the landmark and manages links and separators for you.
AdminCP labels
In the AdminCP, breadcrumbs seamlessly coordinate with the admin navigation
declared in admin/nav.tsx. When a route matches a sidebar item, its
translated label is resolved automatically.