Skip to content

Commit

Permalink
fix(seo): docs breadcrumb structured data should use JSON-LD and filt…
Browse files Browse the repository at this point in the history
…er unliked categories (#10888)

Co-authored-by: sebastien <lorber.sebastien@gmail.com>
  • Loading branch information
johnnyreilly and slorber authored Feb 7, 2025
1 parent cd7875b commit 45065e8
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 59 deletions.
1 change: 1 addition & 0 deletions packages/docusaurus-plugin-content-blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"reading-time": "^1.5.0",
"schema-dts": "^1.1.2",
"srcset": "^4.0.0",
"tslib": "^2.6.0",
"unist-util-visit": "^5.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-plugin-content-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"schema-dts": "^1.1.2",
"tslib": "^2.6.0",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
Expand Down
2 changes: 2 additions & 0 deletions packages/docusaurus-plugin-content-docs/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export {
getDocsVersionSearchTag,
} from './docsSearch';

export {useBreadcrumbsStructuredData} from './structuredDataUtils';

export type ActivePlugin = {
pluginId: string;
pluginData: GlobalPluginData;
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`,
})),
};
}
11 changes: 11 additions & 0 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1846,3 +1846,14 @@ declare module '@theme/DocBreadcrumbs/Items/Home' {

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;
}
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {useHomePageRoute} from '@docusaurus/theme-common/internal';
import Link from '@docusaurus/Link';
import {translate} from '@docusaurus/Translate';
import HomeBreadcrumbItem from '@theme/DocBreadcrumbs/Items/Home';
import DocBreadcrumbsStructuredData from '@theme/DocBreadcrumbs/StructuredData';

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

Expand All @@ -28,22 +29,13 @@ function BreadcrumbsItemLink({
}): ReactNode {
const className = 'breadcrumbs__link';
if (isLast) {
return (
<span className={className} itemProp="name">
{children}
</span>
);
return <span className={className}>{children}</span>;
}
return href ? (
<Link className={className} href={href} itemProp="item">
<span itemProp="name">{children}</span>
<Link className={className} href={href}>
<span>{children}</span>
</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>
);
}
Expand All @@ -52,26 +44,16 @@ function BreadcrumbsItemLink({
function BreadcrumbsItem({
children,
active,
index,
addMicrodata,
}: {
children: ReactNode;
active?: boolean;
index: number;
addMicrodata: boolean;
}): ReactNode {
return (
<li
{...(addMicrodata && {
itemScope: true,
itemProp: 'itemListElement',
itemType: 'https://schema.org/ListItem',
})}
className={clsx('breadcrumbs__item', {
'breadcrumbs__item--active': active,
})}>
{children}
<meta itemProp="position" content={String(index + 1)} />
</li>
);
}
Expand All @@ -85,40 +67,36 @@ export default function DocBreadcrumbs(): ReactNode {
}

return (
<nav
className={clsx(
ThemeClassNames.docs.docBreadcrumbs,
styles.breadcrumbsContainer,
)}
aria-label={translate({
id: 'theme.docs.breadcrumbs.navAriaLabel',
message: 'Breadcrumbs',
description: 'The ARIA label for the breadcrumbs',
})}>
<ul
className="breadcrumbs"
itemScope
itemType="https://schema.org/BreadcrumbList">
{homePageRoute && <HomeBreadcrumbItem />}
{breadcrumbs.map((item, idx) => {
const isLast = idx === breadcrumbs.length - 1;
const href =
item.type === 'category' && item.linkUnlisted
? undefined
: item.href;
return (
<BreadcrumbsItem
key={idx}
active={isLast}
index={idx}
addMicrodata={!!href}>
<BreadcrumbsItemLink href={href} isLast={isLast}>
{item.label}
</BreadcrumbsItemLink>
</BreadcrumbsItem>
);
})}
</ul>
</nav>
<>
<DocBreadcrumbsStructuredData breadcrumbs={breadcrumbs} />
<nav
className={clsx(
ThemeClassNames.docs.docBreadcrumbs,
styles.breadcrumbsContainer,
)}
aria-label={translate({
id: 'theme.docs.breadcrumbs.navAriaLabel',
message: 'Breadcrumbs',
description: 'The ARIA label for the breadcrumbs',
})}>
<ul className="breadcrumbs">
{homePageRoute && <HomeBreadcrumbItem />}
{breadcrumbs.map((item, idx) => {
const isLast = idx === breadcrumbs.length - 1;
const href =
item.type === 'category' && item.linkUnlisted
? undefined
: item.href;
return (
<BreadcrumbsItem key={idx} active={isLast}>
<BreadcrumbsItemLink href={href} isLast={isLast}>
{item.label}
</BreadcrumbsItemLink>
</BreadcrumbsItem>
);
})}
</ul>
</nav>
</>
);
}
3 changes: 1 addition & 2 deletions packages/docusaurus-theme-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@
"@docusaurus/core": "3.7.0",
"@docusaurus/types": "3.7.0",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"schema-dts": "^1.1.2"
"lodash": "^4.17.21"
},
"peerDependencies": {
"@docusaurus/plugin-content-docs": "*",
Expand Down

0 comments on commit 45065e8

Please sign in to comment.