mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-02 03:37:48 +02:00
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:
parent
cd7875bf84
commit
45065e8d2b
8 changed files with 107 additions and 59 deletions
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": "*",
|
||||||
|
|
Loading…
Add table
Reference in a new issue