diff --git a/src/components/LibraryLayout.tsx b/src/components/LibraryLayout.tsx
index 56ab39461..40eba3fb7 100644
--- a/src/components/LibraryLayout.tsx
+++ b/src/components/LibraryLayout.tsx
@@ -1,3 +1,5 @@
+import { Breadcrumbs } from './Breadcrumbs'
+import { getLibraryBreadcrumbItems } from '~/utils/library-breadcrumbs'
import * as React from 'react'
import { CaretLeftIcon } from '@phosphor-icons/react/CaretLeft'
import { CaretRightIcon } from '@phosphor-icons/react/CaretRight'
@@ -816,6 +818,10 @@ export function LibraryLayout({
const { _splat, version: routeVersion } = useParams({ strict: false })
const matches = useMatches()
const lastMatch = last(matches)
+ const landingBreadcrumbItems =
+ lastMatch.status === 'success' && lastMatch.routeId.endsWith('/$version/')
+ ? getLibraryBreadcrumbItems(getLibrary(libraryId))
+ : undefined
const version = getLibraryLayoutVersion({
layoutVersion,
pathname: lastMatch.pathname,
@@ -1545,6 +1551,14 @@ export function LibraryLayout({
'mx-auto w-[var(--docs-content-width,900px)]',
)}
>
+ {landingBreadcrumbItems && (
+
+
+
+ )}
{children}
diff --git a/src/routes/-library-landing-route.tsx b/src/routes/-library-landing-route.tsx
index cfba7f7b3..e85111c74 100644
--- a/src/routes/-library-landing-route.tsx
+++ b/src/routes/-library-landing-route.tsx
@@ -1,3 +1,4 @@
+import { getLibraryBreadcrumbJsonLd } from '~/utils/library-breadcrumbs'
import { redirect } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
import { getLibrary } from '~/libraries'
@@ -59,10 +60,22 @@ export async function loadLibraryLandingRouteData(
}
}
-export function getLibraryLandingHead(libraryId: LandingLibraryId) {
+export function getLibraryLandingHead(
+ libraryId: LandingLibraryId,
+ isSuccess: boolean,
+) {
const library = getLibrary(libraryId)
+ const breadcrumb = isSuccess ? getLibraryBreadcrumbJsonLd(library) : undefined
return {
+ scripts: breadcrumb
+ ? [
+ {
+ type: 'application/ld+json',
+ children: JSON.stringify(breadcrumb).replaceAll('<', '\\u003c'),
+ },
+ ]
+ : [],
meta: seo({
title:
libraryId === 'start'
diff --git a/src/routes/_library/ai.$version.index.tsx b/src/routes/_library/ai.$version.index.tsx
index eacc1ce68..21108c7eb 100644
--- a/src/routes/_library/ai.$version.index.tsx
+++ b/src/routes/_library/ai.$version.index.tsx
@@ -15,7 +15,7 @@ export const Route = createFileRoute('/_library/ai/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('ai', params.version, queryClient),
- head: () => getLibraryLandingHead('ai'),
+ head: ({ match }) => getLibraryLandingHead('ai', match.status === 'success'),
headers: () => getLibraryLandingHeaders('ai'),
component: AiLandingRoute,
})
diff --git a/src/routes/_library/charts.$version.index.tsx b/src/routes/_library/charts.$version.index.tsx
index b1b61cfdc..002613091 100644
--- a/src/routes/_library/charts.$version.index.tsx
+++ b/src/routes/_library/charts.$version.index.tsx
@@ -28,7 +28,8 @@ export const Route = createFileRoute('/_library/charts/$version/')({
return { ...landingData, catalogOrderSeed, catalog }
},
- head: () => getLibraryLandingHead('charts'),
+ head: ({ match }) =>
+ getLibraryLandingHead('charts', match.status === 'success'),
headers: () => getLibraryLandingHeaders('charts'),
component: ChartsLandingRoute,
})
diff --git a/src/routes/_library/cli.$version.index.tsx b/src/routes/_library/cli.$version.index.tsx
index 96def1bd3..c563ac912 100644
--- a/src/routes/_library/cli.$version.index.tsx
+++ b/src/routes/_library/cli.$version.index.tsx
@@ -15,7 +15,7 @@ export const Route = createFileRoute('/_library/cli/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('cli', params.version, queryClient),
- head: () => getLibraryLandingHead('cli'),
+ head: ({ match }) => getLibraryLandingHead('cli', match.status === 'success'),
headers: () => getLibraryLandingHeaders('cli'),
component: CliLandingRoute,
})
diff --git a/src/routes/_library/config.$version.index.tsx b/src/routes/_library/config.$version.index.tsx
index 68abb4455..5c0343fb9 100644
--- a/src/routes/_library/config.$version.index.tsx
+++ b/src/routes/_library/config.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/config/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('config', params.version, queryClient),
- head: () => getLibraryLandingHead('config'),
+ head: ({ match }) =>
+ getLibraryLandingHead('config', match.status === 'success'),
headers: () => getLibraryLandingHeaders('config'),
component: ConfigLandingRoute,
})
diff --git a/src/routes/_library/db.$version.index.tsx b/src/routes/_library/db.$version.index.tsx
index e6357d434..507f138e2 100644
--- a/src/routes/_library/db.$version.index.tsx
+++ b/src/routes/_library/db.$version.index.tsx
@@ -15,7 +15,7 @@ export const Route = createFileRoute('/_library/db/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('db', params.version, queryClient),
- head: () => getLibraryLandingHead('db'),
+ head: ({ match }) => getLibraryLandingHead('db', match.status === 'success'),
headers: () => getLibraryLandingHeaders('db'),
component: DbLandingRoute,
})
diff --git a/src/routes/_library/devtools.$version.index.tsx b/src/routes/_library/devtools.$version.index.tsx
index 499c813fb..88c83b80e 100644
--- a/src/routes/_library/devtools.$version.index.tsx
+++ b/src/routes/_library/devtools.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/devtools/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('devtools', params.version, queryClient),
- head: () => getLibraryLandingHead('devtools'),
+ head: ({ match }) =>
+ getLibraryLandingHead('devtools', match.status === 'success'),
headers: () => getLibraryLandingHeaders('devtools'),
component: DevtoolsLandingRoute,
})
diff --git a/src/routes/_library/form.$version.index.tsx b/src/routes/_library/form.$version.index.tsx
index 24222375f..b014d2d56 100644
--- a/src/routes/_library/form.$version.index.tsx
+++ b/src/routes/_library/form.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/form/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('form', params.version, queryClient),
- head: () => getLibraryLandingHead('form'),
+ head: ({ match }) =>
+ getLibraryLandingHead('form', match.status === 'success'),
headers: () => getLibraryLandingHeaders('form'),
component: FormLandingRoute,
})
diff --git a/src/routes/_library/highlight.$version.index.tsx b/src/routes/_library/highlight.$version.index.tsx
index a3a0ef240..49194aa54 100644
--- a/src/routes/_library/highlight.$version.index.tsx
+++ b/src/routes/_library/highlight.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/highlight/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('highlight', params.version, queryClient),
- head: () => getLibraryLandingHead('highlight'),
+ head: ({ match }) =>
+ getLibraryLandingHead('highlight', match.status === 'success'),
headers: () => getLibraryLandingHeaders('highlight'),
component: HighlightLandingRoute,
})
diff --git a/src/routes/_library/hotkeys.$version.index.tsx b/src/routes/_library/hotkeys.$version.index.tsx
index 4eabcda08..519ee2b98 100644
--- a/src/routes/_library/hotkeys.$version.index.tsx
+++ b/src/routes/_library/hotkeys.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/hotkeys/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('hotkeys', params.version, queryClient),
- head: () => getLibraryLandingHead('hotkeys'),
+ head: ({ match }) =>
+ getLibraryLandingHead('hotkeys', match.status === 'success'),
headers: () => getLibraryLandingHeaders('hotkeys'),
component: HotkeysLandingRoute,
})
diff --git a/src/routes/_library/intent.$version.index.tsx b/src/routes/_library/intent.$version.index.tsx
index 3ea1193c0..9be47a0b7 100644
--- a/src/routes/_library/intent.$version.index.tsx
+++ b/src/routes/_library/intent.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/intent/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('intent', params.version, queryClient),
- head: () => getLibraryLandingHead('intent'),
+ head: ({ match }) =>
+ getLibraryLandingHead('intent', match.status === 'success'),
headers: () => getLibraryLandingHeaders('intent'),
component: IntentLandingRoute,
})
diff --git a/src/routes/_library/markdown.$version.index.tsx b/src/routes/_library/markdown.$version.index.tsx
index 6b1834a74..1481fc0d6 100644
--- a/src/routes/_library/markdown.$version.index.tsx
+++ b/src/routes/_library/markdown.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/markdown/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('markdown', params.version, queryClient),
- head: () => getLibraryLandingHead('markdown'),
+ head: ({ match }) =>
+ getLibraryLandingHead('markdown', match.status === 'success'),
headers: () => getLibraryLandingHeaders('markdown'),
component: MarkdownLandingRoute,
})
diff --git a/src/routes/_library/pacer.$version.index.tsx b/src/routes/_library/pacer.$version.index.tsx
index 3d7a3ac32..bfeb4498e 100644
--- a/src/routes/_library/pacer.$version.index.tsx
+++ b/src/routes/_library/pacer.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/pacer/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('pacer', params.version, queryClient),
- head: () => getLibraryLandingHead('pacer'),
+ head: ({ match }) =>
+ getLibraryLandingHead('pacer', match.status === 'success'),
headers: () => getLibraryLandingHeaders('pacer'),
component: PacerLandingRoute,
})
diff --git a/src/routes/_library/query.$version.index.tsx b/src/routes/_library/query.$version.index.tsx
index 987d8126e..8d79dc2ec 100644
--- a/src/routes/_library/query.$version.index.tsx
+++ b/src/routes/_library/query.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/query/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('query', params.version, queryClient),
- head: () => getLibraryLandingHead('query'),
+ head: ({ match }) =>
+ getLibraryLandingHead('query', match.status === 'success'),
headers: () => getLibraryLandingHeaders('query'),
component: QueryLandingRoute,
})
diff --git a/src/routes/_library/ranger.$version.index.tsx b/src/routes/_library/ranger.$version.index.tsx
index f0a8cede6..4476c46f4 100644
--- a/src/routes/_library/ranger.$version.index.tsx
+++ b/src/routes/_library/ranger.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/ranger/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('ranger', params.version, queryClient),
- head: () => getLibraryLandingHead('ranger'),
+ head: ({ match }) =>
+ getLibraryLandingHead('ranger', match.status === 'success'),
headers: () => getLibraryLandingHeaders('ranger'),
component: RangerLandingRoute,
})
diff --git a/src/routes/_library/redact.$version.index.tsx b/src/routes/_library/redact.$version.index.tsx
index ce5b9acf3..779d8233b 100644
--- a/src/routes/_library/redact.$version.index.tsx
+++ b/src/routes/_library/redact.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/redact/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('redact', params.version, queryClient),
- head: () => getLibraryLandingHead('redact'),
+ head: ({ match }) =>
+ getLibraryLandingHead('redact', match.status === 'success'),
headers: () => getLibraryLandingHeaders('redact'),
component: RedactLandingRoute,
})
diff --git a/src/routes/_library/router.$version.index.tsx b/src/routes/_library/router.$version.index.tsx
index 176ea8a74..6b015ae8c 100644
--- a/src/routes/_library/router.$version.index.tsx
+++ b/src/routes/_library/router.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/router/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('router', params.version, queryClient),
- head: () => getLibraryLandingHead('router'),
+ head: ({ match }) =>
+ getLibraryLandingHead('router', match.status === 'success'),
headers: () => getLibraryLandingHeaders('router'),
component: RouterLandingRoute,
})
diff --git a/src/routes/_library/start.$version.index.tsx b/src/routes/_library/start.$version.index.tsx
index 670dd461f..a2388208d 100644
--- a/src/routes/_library/start.$version.index.tsx
+++ b/src/routes/_library/start.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/start/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('start', params.version, queryClient),
- head: () => getLibraryLandingHead('start'),
+ head: ({ match }) =>
+ getLibraryLandingHead('start', match.status === 'success'),
headers: () => getLibraryLandingHeaders('start'),
component: StartLandingRoute,
})
diff --git a/src/routes/_library/store.$version.index.tsx b/src/routes/_library/store.$version.index.tsx
index e2fed0eaf..1658d7478 100644
--- a/src/routes/_library/store.$version.index.tsx
+++ b/src/routes/_library/store.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/store/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('store', params.version, queryClient),
- head: () => getLibraryLandingHead('store'),
+ head: ({ match }) =>
+ getLibraryLandingHead('store', match.status === 'success'),
headers: () => getLibraryLandingHeaders('store'),
component: StoreLandingRoute,
})
diff --git a/src/routes/_library/table.$version.index.tsx b/src/routes/_library/table.$version.index.tsx
index c68796601..ce86e63f4 100644
--- a/src/routes/_library/table.$version.index.tsx
+++ b/src/routes/_library/table.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/table/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('table', params.version, queryClient),
- head: () => getLibraryLandingHead('table'),
+ head: ({ match }) =>
+ getLibraryLandingHead('table', match.status === 'success'),
headers: () => getLibraryLandingHeaders('table'),
component: TableLandingRoute,
})
diff --git a/src/routes/_library/virtual.$version.index.tsx b/src/routes/_library/virtual.$version.index.tsx
index fa727d38d..8ae1526bc 100644
--- a/src/routes/_library/virtual.$version.index.tsx
+++ b/src/routes/_library/virtual.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/virtual/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('virtual', params.version, queryClient),
- head: () => getLibraryLandingHead('virtual'),
+ head: ({ match }) =>
+ getLibraryLandingHead('virtual', match.status === 'success'),
headers: () => getLibraryLandingHeaders('virtual'),
component: VirtualLandingRoute,
})
diff --git a/src/routes/_library/workflow.$version.index.tsx b/src/routes/_library/workflow.$version.index.tsx
index 8efe9cc89..289508cb1 100644
--- a/src/routes/_library/workflow.$version.index.tsx
+++ b/src/routes/_library/workflow.$version.index.tsx
@@ -15,7 +15,8 @@ export const Route = createFileRoute('/_library/workflow/$version/')({
},
loader: ({ params, context: { queryClient } }) =>
loadLibraryLandingRouteData('workflow', params.version, queryClient),
- head: () => getLibraryLandingHead('workflow'),
+ head: ({ match }) =>
+ getLibraryLandingHead('workflow', match.status === 'success'),
headers: () => getLibraryLandingHeaders('workflow'),
component: WorkflowLandingRoute,
})
diff --git a/src/utils/library-breadcrumbs.ts b/src/utils/library-breadcrumbs.ts
new file mode 100644
index 000000000..2e75e352a
--- /dev/null
+++ b/src/utils/library-breadcrumbs.ts
@@ -0,0 +1,29 @@
+import type { BreadcrumbItem } from '~/components/Breadcrumbs'
+import { canonicalUrl } from './seo'
+
+type LibraryBreadcrumbData = { name: string; to?: string; visible?: boolean }
+
+export function getLibraryBreadcrumbItems(
+ library: LibraryBreadcrumbData,
+): Array | undefined {
+ if (library.visible === false || !library.to) return undefined
+ return [
+ { label: 'Libraries', to: '/libraries' },
+ { label: library.name, to: library.to },
+ ]
+}
+
+export function getLibraryBreadcrumbJsonLd(library: LibraryBreadcrumbData) {
+ const items = getLibraryBreadcrumbItems(library)
+ if (!items) return undefined
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: items.map((item, index) => ({
+ '@type': 'ListItem',
+ position: index + 1,
+ name: item.label,
+ item: canonicalUrl(item.to),
+ })),
+ }
+}
diff --git a/tests/library-breadcrumbs.test.ts b/tests/library-breadcrumbs.test.ts
new file mode 100644
index 000000000..855e4f704
--- /dev/null
+++ b/tests/library-breadcrumbs.test.ts
@@ -0,0 +1,44 @@
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+import { query, workflow } from '../src/libraries'
+import {
+ getLibraryBreadcrumbItems,
+ getLibraryBreadcrumbJsonLd,
+} from '../src/utils/library-breadcrumbs'
+Object.assign(globalThis, { __TANSTACK_SITE_URL__: 'https://tanstack.com' })
+
+test('public library landing trail shares labels and canonical destinations', () => {
+ assert.deepEqual(getLibraryBreadcrumbItems(query), [
+ { label: 'Libraries', to: '/libraries' },
+ { label: 'TanStack Query', to: '/query/latest' },
+ ])
+ assert.deepEqual(getLibraryBreadcrumbJsonLd(query)?.itemListElement, [
+ {
+ '@type': 'ListItem',
+ position: 1,
+ name: 'Libraries',
+ item: 'https://tanstack.com/libraries',
+ },
+ {
+ '@type': 'ListItem',
+ position: 2,
+ name: 'TanStack Query',
+ item: 'https://tanstack.com/query/latest',
+ },
+ ])
+})
+test('hidden library emits neither a visible trail nor structured data', () => {
+ assert.equal(getLibraryBreadcrumbItems(workflow), undefined)
+ assert.equal(getLibraryBreadcrumbJsonLd(workflow), undefined)
+})
+
+test('library without a landing destination has no breadcrumb trail', () => {
+ assert.equal(
+ getLibraryBreadcrumbItems({ name: 'Unlinked library' }),
+ undefined,
+ )
+ assert.equal(
+ getLibraryBreadcrumbJsonLd({ name: 'Unlinked library' }),
+ undefined,
+ )
+})