Routing
Breadcrumbs
Add localized crumbs to the VitNode breadcrumb trail from plugin routes, layouts, AdminCP pages and the app's own file routes.
VitNode renders the breadcrumb trail in the public header and in the AdminCP shell. Each route that declares a crumb adds one entry, and the trail reads from parent to child:
Site notes / Office closed on FridayA route declares only the label of its own crumb. The shell renders the <nav> landmark, the separators, the locale-aware links and aria-current="page" on the last crumb.
Add a crumb to a plugin route
Set breadcrumb in the page module's route to a component that returns the label. It renders inside the route's declared messages, so useTranslations works:
import { defineRoute } from '@vitnode/core/tanstack/plugin-routes'
import { useTranslations } from 'use-intl'
const NotesCrumb = () => {
const t = useTranslations('site-notes.notes')
return t('title')
}
export const route = defineRoute({
breadcrumb: NotesCrumb,
})Use loader data in a crumb
A crumb receives { loaderData, params, search }, the same values the page gets. On a dynamic page, show the record's name instead of its id, without a second request. This extends the note page from Add a route, where loadNote and NotePageData are defined:
export const route = defineRoute({
load: async ({ context, params }) =>
await loadNote(context.queryClient, params.id),
head: ({ loaderData }) => ({
title: loaderData?.title,
description: loaderData?.body,
}),
breadcrumb: ({ loaderData }) => loaderData.title,
})Declare breadcrumb after load, so TypeScript types loaderData from the loader. A crumb sits under every crumb of the routes above it. When the list and the note page are siblings rather than a layout and its child, the trail shows only the note. Put both under a layout() with its own crumb to get Site notes / Office closed on Friday.
Leave a route out
A route without breadcrumb adds nothing, and the crumbs above it stay. Write breadcrumb: false (or null) to make that explicit, for example on an index() page whose layout already names the screen. The trail then ends at the deepest route that declares a crumb, and that crumb gets aria-current="page".
Several crumbs from one route
Some pages know more of the trail than their own label. A forum topic, for example, can list every parent forum from its loader data. Declare a group with routeBreadcrumbGroup from @vitnode/core/tanstack/plugin-routes, and render the items yourself with the breadcrumb components:
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@vitnode/core/components/ui/breadcrumb'
import { HrefLink } from '@vitnode/core/tanstack/link'
import { defineRoute, routeBreadcrumbGroup } from '@vitnode/core/tanstack/plugin-routes'
export const route = defineRoute({
load: async ({ context, params }) =>
await loadNote(context.queryClient, params.id),
breadcrumb: routeBreadcrumbGroup<NotePageData>(({ loaderData }) => (
<>
<BreadcrumbItem>
<BreadcrumbLink render={<HrefLink href="/site-notes/notes" />}>
Site notes
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{loaderData.title}</BreadcrumbPage>
</BreadcrumbItem>
</>
)),
})The shell adds the separator before the group. Inside it, you add the separators between your own items and mark the last one with BreadcrumbPage. Translate the labels with useTranslations in a small component, as in the first example.
A plain breadcrumb component returns text or an inline element, such as an
icon with text. Never render <nav>, links or separators in it, because the
shell already does. Only a group renders BreadcrumbItem and friends.
AdminCP crumbs
An AdminCP route declares a crumb the same way. To label the crumbs from the AdminCP navigation, so they match the sidebar, use adminBreadcrumb from @vitnode/core/tanstack/admin. segments is the path below /admin:
import { definePluginRoute } from '@vitnode/core/routing'
import { adminBreadcrumb } from '@vitnode/core/tanstack/admin'
export const route = definePluginRoute({
breadcrumb: adminBreadcrumb({ segments: ['site-notes'] }),
})Each segment gets the title of the navigation entry whose link matches it, and a segment with no entry gets a readable version of its name. Pass overrideLastLabel when the page knows a better name for the last crumb, such as a record's title. The AdminCP does not look labels up on its own, so a route without breadcrumb adds no crumb there either.
Crumbs on the app's own routes
A file route in your app declares its crumb as staticData.breadcrumb. App routes do not get plugin messages, so a crumb that renders strings wraps itself in RouteMessages from @vitnode/core/tanstack/i18n:
import { RouteMessages } from '@vitnode/core/tanstack/i18n'
import { useTranslations } from 'use-intl'
const PricingLabel = () => {
const t = useTranslations('site.pricing')
return t('title')
}
export const PricingBreadcrumb = () => (
<RouteMessages namespaces={['site.pricing']}>
<PricingLabel />
</RouteMessages>
)import { createFileRoute } from '@tanstack/react-router'
import { PricingBreadcrumb } from '@/site/pricing/breadcrumb'
import { PricingPage } from '@/site/pricing/pricing-page'
export const Route = createFileRoute('/_main/pricing')({
staticData: { breadcrumb: <PricingBreadcrumb /> },
component: PricingPage,
})site.pricing is a namespace from your app's own locale files; see Your app's own strings.
Related
- Layouts: one crumb for a layout and all its pages.
- AdminCP pages: a complete AdminCP screen.