Skip to content

Commit 45065e8

Browse files
fix(seo): docs breadcrumb structured data should use JSON-LD and filter unliked categories (#10888)
Co-authored-by: sebastien <[email protected]>
1 parent cd7875b commit 45065e8

File tree

8 files changed

+107
-59
lines changed

8 files changed

+107
-59
lines changed

packages/docusaurus-plugin-content-blog/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"fs-extra": "^11.1.1",
4545
"lodash": "^4.17.21",
4646
"reading-time": "^1.5.0",
47+
"schema-dts": "^1.1.2",
4748
"srcset": "^4.0.0",
4849
"tslib": "^2.6.0",
4950
"unist-util-visit": "^5.0.0",

packages/docusaurus-plugin-content-docs/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"fs-extra": "^11.1.1",
5050
"js-yaml": "^4.1.0",
5151
"lodash": "^4.17.21",
52+
"schema-dts": "^1.1.2",
5253
"tslib": "^2.6.0",
5354
"utility-types": "^3.10.0",
5455
"webpack": "^5.88.1"

packages/docusaurus-plugin-content-docs/src/client/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export {
6060
getDocsVersionSearchTag,
6161
} from './docsSearch';
6262

63+
export {useBreadcrumbsStructuredData} from './structuredDataUtils';
64+
6365
export type ActivePlugin = {
6466
pluginId: string;
6567
pluginData: GlobalPluginData;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
9+
import type {PropSidebarBreadcrumbsItem} from '@docusaurus/plugin-content-docs';
10+
import type {WithContext, BreadcrumbList} from 'schema-dts';
11+
12+
export function useBreadcrumbsStructuredData({
13+
breadcrumbs,
14+
}: {
15+
breadcrumbs: PropSidebarBreadcrumbsItem[];
16+
}): WithContext<BreadcrumbList> {
17+
const {siteConfig} = useDocusaurusContext();
18+
return {
19+
'@context': 'https://schema.org',
20+
'@type': 'BreadcrumbList',
21+
itemListElement: breadcrumbs
22+
// We filter breadcrumb items without links, they are not allowed
23+
// See also https://github.com/facebook/docusaurus/issues/9319#issuecomment-2643560845
24+
.filter((breadcrumb) => breadcrumb.href)
25+
.map((breadcrumb, index) => ({
26+
'@type': 'ListItem',
27+
position: index + 1,
28+
name: breadcrumb.label,
29+
item: `${siteConfig.url}${breadcrumb.href}`,
30+
})),
31+
};
32+
}

packages/docusaurus-theme-classic/src/theme-classic.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1846,3 +1846,14 @@ declare module '@theme/DocBreadcrumbs/Items/Home' {
18461846

18471847
export default function HomeBreadcrumbItem(): ReactNode;
18481848
}
1849+
1850+
declare module '@theme/DocBreadcrumbs/StructuredData' {
1851+
import type {ReactNode} from 'react';
1852+
import type {PropSidebarBreadcrumbsItem} from '@docusaurus/plugin-content-docs';
1853+
1854+
export interface Props {
1855+
readonly breadcrumbs: PropSidebarBreadcrumbsItem[];
1856+
}
1857+
1858+
export default function DocBreadcrumbsStructuredData(props: Props): ReactNode;
1859+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React, {type ReactNode} from 'react';
9+
import Head from '@docusaurus/Head';
10+
import {useBreadcrumbsStructuredData} from '@docusaurus/plugin-content-docs/client';
11+
import type {Props} from '@theme/DocBreadcrumbs/StructuredData';
12+
13+
export default function DocBreadcrumbsStructuredData(props: Props): ReactNode {
14+
const structuredData = useBreadcrumbsStructuredData({
15+
breadcrumbs: props.breadcrumbs,
16+
});
17+
return (
18+
<Head>
19+
<script type="application/ld+json">
20+
{JSON.stringify(structuredData)}
21+
</script>
22+
</Head>
23+
);
24+
}

packages/docusaurus-theme-classic/src/theme/DocBreadcrumbs/index.tsx

+35-57
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {useHomePageRoute} from '@docusaurus/theme-common/internal';
1313
import Link from '@docusaurus/Link';
1414
import {translate} from '@docusaurus/Translate';
1515
import HomeBreadcrumbItem from '@theme/DocBreadcrumbs/Items/Home';
16+
import DocBreadcrumbsStructuredData from '@theme/DocBreadcrumbs/StructuredData';
1617

1718
import styles from './styles.module.css';
1819

@@ -28,22 +29,13 @@ function BreadcrumbsItemLink({
2829
}): ReactNode {
2930
const className = 'breadcrumbs__link';
3031
if (isLast) {
31-
return (
32-
<span className={className} itemProp="name">
33-
{children}
34-
</span>
35-
);
32+
return <span className={className}>{children}</span>;
3633
}
3734
return href ? (
38-
<Link className={className} href={href} itemProp="item">
39-
<span itemProp="name">{children}</span>
35+
<Link className={className} href={href}>
36+
<span>{children}</span>
4037
</Link>
4138
) : (
42-
// TODO Google search console doesn't like breadcrumb items without href.
43-
// The schema doesn't seem to require `id` for each `item`, although Google
44-
// insist to infer one, even if it's invalid. Removing `itemProp="item
45-
// name"` for now, since I don't know how to properly fix it.
46-
// See https://github.com/facebook/docusaurus/issues/7241
4739
<span className={className}>{children}</span>
4840
);
4941
}
@@ -52,26 +44,16 @@ function BreadcrumbsItemLink({
5244
function BreadcrumbsItem({
5345
children,
5446
active,
55-
index,
56-
addMicrodata,
5747
}: {
5848
children: ReactNode;
5949
active?: boolean;
60-
index: number;
61-
addMicrodata: boolean;
6250
}): ReactNode {
6351
return (
6452
<li
65-
{...(addMicrodata && {
66-
itemScope: true,
67-
itemProp: 'itemListElement',
68-
itemType: 'https://schema.org/ListItem',
69-
})}
7053
className={clsx('breadcrumbs__item', {
7154
'breadcrumbs__item--active': active,
7255
})}>
7356
{children}
74-
<meta itemProp="position" content={String(index + 1)} />
7557
</li>
7658
);
7759
}
@@ -85,40 +67,36 @@ export default function DocBreadcrumbs(): ReactNode {
8567
}
8668

8769
return (
88-
<nav
89-
className={clsx(
90-
ThemeClassNames.docs.docBreadcrumbs,
91-
styles.breadcrumbsContainer,
92-
)}
93-
aria-label={translate({
94-
id: 'theme.docs.breadcrumbs.navAriaLabel',
95-
message: 'Breadcrumbs',
96-
description: 'The ARIA label for the breadcrumbs',
97-
})}>
98-
<ul
99-
className="breadcrumbs"
100-
itemScope
101-
itemType="https://schema.org/BreadcrumbList">
102-
{homePageRoute && <HomeBreadcrumbItem />}
103-
{breadcrumbs.map((item, idx) => {
104-
const isLast = idx === breadcrumbs.length - 1;
105-
const href =
106-
item.type === 'category' && item.linkUnlisted
107-
? undefined
108-
: item.href;
109-
return (
110-
<BreadcrumbsItem
111-
key={idx}
112-
active={isLast}
113-
index={idx}
114-
addMicrodata={!!href}>
115-
<BreadcrumbsItemLink href={href} isLast={isLast}>
116-
{item.label}
117-
</BreadcrumbsItemLink>
118-
</BreadcrumbsItem>
119-
);
120-
})}
121-
</ul>
122-
</nav>
70+
<>
71+
<DocBreadcrumbsStructuredData breadcrumbs={breadcrumbs} />
72+
<nav
73+
className={clsx(
74+
ThemeClassNames.docs.docBreadcrumbs,
75+
styles.breadcrumbsContainer,
76+
)}
77+
aria-label={translate({
78+
id: 'theme.docs.breadcrumbs.navAriaLabel',
79+
message: 'Breadcrumbs',
80+
description: 'The ARIA label for the breadcrumbs',
81+
})}>
82+
<ul className="breadcrumbs">
83+
{homePageRoute && <HomeBreadcrumbItem />}
84+
{breadcrumbs.map((item, idx) => {
85+
const isLast = idx === breadcrumbs.length - 1;
86+
const href =
87+
item.type === 'category' && item.linkUnlisted
88+
? undefined
89+
: item.href;
90+
return (
91+
<BreadcrumbsItem key={idx} active={isLast}>
92+
<BreadcrumbsItemLink href={href} isLast={isLast}>
93+
{item.label}
94+
</BreadcrumbsItemLink>
95+
</BreadcrumbsItem>
96+
);
97+
})}
98+
</ul>
99+
</nav>
100+
</>
123101
);
124102
}

packages/docusaurus-theme-common/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@
4747
"@docusaurus/core": "3.7.0",
4848
"@docusaurus/types": "3.7.0",
4949
"fs-extra": "^11.1.1",
50-
"lodash": "^4.17.21",
51-
"schema-dts": "^1.1.2"
50+
"lodash": "^4.17.21"
5251
},
5352
"peerDependencies": {
5453
"@docusaurus/plugin-content-docs": "*",

0 commit comments

Comments
 (0)