diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 3babe6f8e..042dce74f 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -9,10 +9,17 @@ import { DropdownItem, } from './Dropdown' +export type BreadcrumbItem = { + label: string + to: string + search?: true + preload?: false +} + type BreadcrumbsProps = { /** Section label (e.g., "Getting Started", "Blog") */ section: string - items?: Array<{ label: string; to: string }> + items?: Array /** Optional link for the section */ sectionTo?: string headings?: MarkdownHeading[] @@ -51,6 +58,8 @@ export function Breadcrumbs({ ) : ( diff --git a/src/components/charts/ChartsCatalogPages.tsx b/src/components/charts/ChartsCatalogPages.tsx index dadb1c9fb..e60b29150 100644 --- a/src/components/charts/ChartsCatalogPages.tsx +++ b/src/components/charts/ChartsCatalogPages.tsx @@ -1,3 +1,5 @@ +import { Breadcrumbs } from '~/components/Breadcrumbs' +import { getChartBreadcrumbItems } from '~/utils/chart-breadcrumbs' import { ArrowsOutSimpleIcon, ArrowUpRightIcon, @@ -9,7 +11,6 @@ import type { ChartsCatalogIndexCase } from '~/utils/charts-catalog-index' import type { ExampleDefinition } from '~/utils/example-workspace' import { chartsCatalogCollections, - findChartsCatalogCollection, type ChartsCatalogAuthoredSource, } from '~/utils/charts-catalog' import { ChartsCatalogPreview } from './ChartsCatalogPreview' @@ -161,34 +162,14 @@ export function ChartsCatalogDetail({ } } }) { - const collection = catalogCase.collection - ? findChartsCatalogCollection(catalogCase.collection) - : undefined - return (
- {collection ? ( - - {collection.title} - - ) : ( - - Catalog - - )} +

{catalogCase.title}

diff --git a/src/routes/_library/charts.catalog.charts.$caseId.tsx b/src/routes/_library/charts.catalog.charts.$caseId.tsx index ba1b6fc46..5479fd3b2 100644 --- a/src/routes/_library/charts.catalog.charts.$caseId.tsx +++ b/src/routes/_library/charts.catalog.charts.$caseId.tsx @@ -1,3 +1,4 @@ +import { getChartBreadcrumbJsonLd } from '~/utils/chart-breadcrumbs' import { createFileRoute, notFound } from '@tanstack/react-router' import { ChartsCatalogDetail, @@ -21,6 +22,16 @@ export const Route = createFileRoute('/_library/charts/catalog/charts/$caseId')( pendingMs: 250, component: ChartsCatalogCaseRoute, head: ({ loaderData }) => ({ + scripts: loaderData + ? [ + { + type: 'application/ld+json', + children: JSON.stringify( + getChartBreadcrumbJsonLd(loaderData.case), + ).replaceAll('<', '\\u003c'), + }, + ] + : [], meta: seo({ title: loaderData ? `${loaderData.case.title} | TanStack Charts` diff --git a/src/utils/chart-breadcrumbs.ts b/src/utils/chart-breadcrumbs.ts new file mode 100644 index 000000000..c978a2b5a --- /dev/null +++ b/src/utils/chart-breadcrumbs.ts @@ -0,0 +1,39 @@ +import type { BreadcrumbItem } from '~/components/Breadcrumbs' +import { findChartsCatalogCollection } from './charts-catalog' +import { canonicalUrl } from './seo' + +type ChartBreadcrumbData = { id: string; title: string; collection?: string } + +export function getChartBreadcrumbItems( + chart: ChartBreadcrumbData, +): Array { + const collection = chart.collection + ? findChartsCatalogCollection(chart.collection) + : undefined + const items: Array = [ + { label: 'Catalog', to: '/charts/catalog', search: true, preload: false }, + ] + if (collection) { + items.push({ + label: collection.title, + to: `/charts/catalog/collections/${collection.id}`, + search: true, + preload: false, + }) + } + items.push({ label: chart.title, to: `/charts/catalog/charts/${chart.id}` }) + return items +} + +export function getChartBreadcrumbJsonLd(chart: ChartBreadcrumbData) { + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: getChartBreadcrumbItems(chart).map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.label, + item: canonicalUrl(item.to), + })), + } +} diff --git a/tests/chart-breadcrumbs.test.ts b/tests/chart-breadcrumbs.test.ts new file mode 100644 index 000000000..c339a4117 --- /dev/null +++ b/tests/chart-breadcrumbs.test.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { + getChartBreadcrumbItems, + getChartBreadcrumbJsonLd, +} from '../src/utils/chart-breadcrumbs' +Object.assign(globalThis, { __TANSTACK_SITE_URL__: 'https://tanstack.com' }) + +test('chart hierarchy includes only known collections and keeps navigation options out of canonical URLs', () => { + const chart = { + id: 'area-simple', + title: 'Area ', + collection: 'shadcn', + } + const items = getChartBreadcrumbItems(chart) + assert.deepEqual( + items.map((item) => item.label), + ['Catalog', 'shadcn/ui Charts', 'Area '], + ) + assert.equal(items[1].search, true) + assert.equal(items[1].preload, false) + const schema = getChartBreadcrumbJsonLd(chart) + assert.deepEqual( + schema.itemListElement.map((item) => item.position), + [1, 2, 3], + ) + assert.equal( + schema.itemListElement[2].item, + 'https://tanstack.com/charts/catalog/charts/area-simple', + ) + for (const collection of [undefined, 'unknown']) { + assert.equal(getChartBreadcrumbItems({ ...chart, collection }).length, 2) + } +})