fix(seo): docs breadcrumb structured data should use JSON-LD and filter unliked categories (#10888)

Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
John Reilly 2025-02-07 18:03:10 +00:00 committed by GitHub
parent cd7875bf84
commit 45065e8d2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 107 additions and 59 deletions

View file

@ -44,6 +44,7 @@
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"schema-dts": "^1.1.2",
"srcset": "^4.0.0", "srcset": "^4.0.0",
"tslib": "^2.6.0", "tslib": "^2.6.0",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",

View file

@ -49,6 +49,7 @@
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"schema-dts": "^1.1.2",
"tslib": "^2.6.0", "tslib": "^2.6.0",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"webpack": "^5.88.1" "webpack": "^5.88.1"

View file

@ -60,6 +60,8 @@ export {
getDocsVersionSearchTag, getDocsVersionSearchTag,
} from './docsSearch'; } from './docsSearch';
export {useBreadcrumbsStructuredData} from './structuredDataUtils';
export type ActivePlugin = { export type ActivePlugin = {
pluginId: string; pluginId: string;
pluginData: GlobalPluginData; pluginData: GlobalPluginData;

View file

@ -0,0 +1,32 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import type {PropSidebarBreadcrumbsItem} from '@docusaurus/plugin-content-docs';
import type {WithContext, BreadcrumbList} from 'schema-dts';
export function useBreadcrumbsStructuredData({
breadcrumbs,
}: {
breadcrumbs: PropSidebarBreadcrumbsItem[];
}): WithContext<BreadcrumbList> {
const {siteConfig} = useDocusaurusContext();
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumbs
// We filter breadcrumb items without links, they are not allowed
// See also https://github.com/facebook/docusaurus/issues/9319#issuecomment-2643560845
.filter((breadcrumb) => breadcrumb.href)
.map((breadcrumb, index) => ({
'@type': 'ListItem',
position: index + 1,
name: breadcrumb.label,
item: `${siteConfig.url}${breadcrumb.href}`,
})),
};
}

View file

@ -1846,3 +1846,14 @@ declare module '@theme/DocBreadcrumbs/Items/Home' {
export default function HomeBreadcrumbItem(): ReactNode; export default function HomeBreadcrumbItem(): ReactNode;
} }
declare module '@theme/DocBreadcrumbs/StructuredData' {
import type {ReactNode} from 'react';
import type {PropSidebarBreadcrumbsItem} from '@docusaurus/plugin-content-docs';
export interface Props {
readonly breadcrumbs: PropSidebarBreadcrumbsItem[];
}
export default function DocBreadcrumbsStructuredData(props: Props): ReactNode;
}

View file

@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {type ReactNode} from 'react';
import Head from '@docusaurus/Head';
import {useBreadcrumbsStructuredData} from '@docusaurus/plugin-content-docs/client';
import type {Props} from '@theme/DocBreadcrumbs/StructuredData';
export default function DocBreadcrumbsStructuredData(props: Props): ReactNode {
const structuredData = useBreadcrumbsStructuredData({
breadcrumbs: props.breadcrumbs,
});
return (
<Head>
<script type="application/ld+json">
{JSON.stringify(structuredData)}
</script>
</Head>
);
}

View file

@ -13,6 +13,7 @@ import {useHomePageRoute} from '@docusaurus/theme-common/internal';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import {translate} from '@docusaurus/Translate'; import {translate} from '@docusaurus/Translate';
import HomeBreadcrumbItem from '@theme/DocBreadcrumbs/Items/Home'; import HomeBreadcrumbItem from '@theme/DocBreadcrumbs/Items/Home';
import DocBreadcrumbsStructuredData from '@theme/DocBreadcrumbs/StructuredData';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -28,22 +29,13 @@ function BreadcrumbsItemLink({
}): ReactNode { }): ReactNode {
const className = 'breadcrumbs__link'; const className = 'breadcrumbs__link';
if (isLast) { if (isLast) {
return ( return <span className={className}>{children}</span>;
<span className={className} itemProp="name">
{children}
</span>
);
} }
return href ? ( return href ? (
<Link className={className} href={href} itemProp="item"> <Link className={className} href={href}>
<span itemProp="name">{children}</span> <span>{children}</span>
</Link> </Link>
) : ( ) : (
// TODO Google search console doesn't like breadcrumb items without href.
// The schema doesn't seem to require `id` for each `item`, although Google
// insist to infer one, even if it's invalid. Removing `itemProp="item
// name"` for now, since I don't know how to properly fix it.
// See https://github.com/facebook/docusaurus/issues/7241
<span className={className}>{children}</span> <span className={className}>{children}</span>
); );
} }
@ -52,26 +44,16 @@ function BreadcrumbsItemLink({
function BreadcrumbsItem({ function BreadcrumbsItem({
children, children,
active, active,
index,
addMicrodata,
}: { }: {
children: ReactNode; children: ReactNode;
active?: boolean; active?: boolean;
index: number;
addMicrodata: boolean;
}): ReactNode { }): ReactNode {
return ( return (
<li <li
{...(addMicrodata && {
itemScope: true,
itemProp: 'itemListElement',
itemType: 'https://schema.org/ListItem',
})}
className={clsx('breadcrumbs__item', { className={clsx('breadcrumbs__item', {
'breadcrumbs__item--active': active, 'breadcrumbs__item--active': active,
})}> })}>
{children} {children}
<meta itemProp="position" content={String(index + 1)} />
</li> </li>
); );
} }
@ -85,6 +67,8 @@ export default function DocBreadcrumbs(): ReactNode {
} }
return ( return (
<>
<DocBreadcrumbsStructuredData breadcrumbs={breadcrumbs} />
<nav <nav
className={clsx( className={clsx(
ThemeClassNames.docs.docBreadcrumbs, ThemeClassNames.docs.docBreadcrumbs,
@ -95,10 +79,7 @@ export default function DocBreadcrumbs(): ReactNode {
message: 'Breadcrumbs', message: 'Breadcrumbs',
description: 'The ARIA label for the breadcrumbs', description: 'The ARIA label for the breadcrumbs',
})}> })}>
<ul <ul className="breadcrumbs">
className="breadcrumbs"
itemScope
itemType="https://schema.org/BreadcrumbList">
{homePageRoute && <HomeBreadcrumbItem />} {homePageRoute && <HomeBreadcrumbItem />}
{breadcrumbs.map((item, idx) => { {breadcrumbs.map((item, idx) => {
const isLast = idx === breadcrumbs.length - 1; const isLast = idx === breadcrumbs.length - 1;
@ -107,11 +88,7 @@ export default function DocBreadcrumbs(): ReactNode {
? undefined ? undefined
: item.href; : item.href;
return ( return (
<BreadcrumbsItem <BreadcrumbsItem key={idx} active={isLast}>
key={idx}
active={isLast}
index={idx}
addMicrodata={!!href}>
<BreadcrumbsItemLink href={href} isLast={isLast}> <BreadcrumbsItemLink href={href} isLast={isLast}>
{item.label} {item.label}
</BreadcrumbsItemLink> </BreadcrumbsItemLink>
@ -120,5 +97,6 @@ export default function DocBreadcrumbs(): ReactNode {
})} })}
</ul> </ul>
</nav> </nav>
</>
); );
} }

View file

@ -47,8 +47,7 @@
"@docusaurus/core": "3.7.0", "@docusaurus/core": "3.7.0",
"@docusaurus/types": "3.7.0", "@docusaurus/types": "3.7.0",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21"
"schema-dts": "^1.1.2"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/plugin-content-docs": "*", "@docusaurus/plugin-content-docs": "*",