diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 36028495381c..8222431490ee 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -217,6 +217,7 @@ async function processBlogSourceFile( truncateMarker, showReadingTime, editUrl, + blogTitle: baseBlogTitle, } = options; // Lookup in localized folder in priority @@ -314,6 +315,8 @@ async function processBlogSourceFile( return undefined; } + const baseBlogPermalink = normalizeUrl([baseUrl, routeBasePath]); + const tagsBasePath = normalizeUrl([ baseUrl, routeBasePath, @@ -325,6 +328,8 @@ async function processBlogSourceFile( id: slug, metadata: { permalink, + baseBlogPermalink, + baseBlogTitle, editUrl: getBlogEditUrl(), source: aliasedSource, title, diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index b1915f75196f..6d9fa2db921b 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -197,6 +197,10 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the readonly formattedDate: string; /** Full link including base URL. */ readonly permalink: string; + /** the path to the base of the blog */ + readonly baseBlogPermalink: string; + /** title of the overall blog */ + readonly baseBlogTitle: string; /** * Description used in the meta. Could be an empty string (empty content) */ @@ -552,6 +556,27 @@ declare module '@theme/BlogPostPage/Metadata' { export default function BlogPostPageMetadata(): JSX.Element; } +declare module '@theme/BlogPostPage/StructuredData' { + import type { + BlogPostFrontMatter, + PropBlogPostContent, + } from '@docusaurus/plugin-content-blog'; + + export type FrontMatter = BlogPostFrontMatter; + + export type Assets = PropBlogPostContent['assets']; + + export type Metadata = PropBlogPostContent['metadata']; + + export interface Props { + readonly assets: Assets; + readonly frontMatter: FrontMatter; + readonly metadata: Metadata; + } + + export default function BlogPostStructuredData(props: Props): JSX.Element; +} + declare module '@theme/BlogListPage' { import type {Content} from '@theme/BlogPostPage'; import type { @@ -574,6 +599,28 @@ declare module '@theme/BlogListPage' { export default function BlogListPage(props: Props): JSX.Element; } +declare module '@theme/BlogListPage/StructuredData' { + import type {Content} from '@theme/BlogPostPage'; + import type { + BlogSidebar, + BlogPaginatedMetadata, + } from '@docusaurus/plugin-content-blog'; + + export interface Props { + /** Blog sidebar. */ + readonly sidebar: BlogSidebar; + /** Metadata of the current listing page. */ + readonly metadata: BlogPaginatedMetadata; + /** + * Array of blog posts included on this page. Every post's metadata is also + * available. + */ + readonly items: readonly {readonly content: Content}[]; + } + + export default function BlogListPageStructuredData(props: Props): JSX.Element; +} + declare module '@theme/BlogTagsListPage' { import type {BlogSidebar} from '@docusaurus/plugin-content-blog'; import type {TagsListItem} from '@docusaurus/utils'; diff --git a/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx index 60f9b5e2833f..45dbbb2d2546 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogLayout/index.tsx @@ -25,9 +25,7 @@ export default function BlogLayout(props: Props): JSX.Element { className={clsx('col', { 'col--7': hasSidebar, 'col--9 col--offset-1': !hasSidebar, - })} - itemScope - itemType="https://schema.org/Blog"> + })}> {children} {toc &&
{toc}
} diff --git a/packages/docusaurus-theme-classic/src/theme/BlogListPage/StructuredData/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogListPage/StructuredData/index.tsx new file mode 100644 index 000000000000..13cda428422a --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/BlogListPage/StructuredData/index.tsx @@ -0,0 +1,95 @@ +/** + * 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 from 'react'; +import {useBaseUrlUtils} from '@docusaurus/useBaseUrl'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import type {Props} from '@theme/BlogListPage/StructuredData'; + +export default function BlogListPageStructuredData(props: Props): JSX.Element { + const {siteConfig} = useDocusaurusContext(); + const {withBaseUrl} = useBaseUrlUtils(); + + const { + metadata: {blogDescription, blogTitle, permalink}, + } = props; + + const url = `${siteConfig.url}${permalink}`; + + // details on structured data support: https://schema.org/Blog + const blogStructuredData = { + '@context': 'https://schema.org', + '@type': 'Blog', + '@id': url, + mainEntityOfPage: url, + headline: blogTitle, + description: blogDescription, + blogPost: props.items.map((blogItem) => { + const { + content: {assets, frontMatter, metadata}, + } = blogItem; + const {date, title, description} = metadata; + + const image = assets.image ?? frontMatter.image; + const keywords = frontMatter.keywords ?? []; + + // an array of https://schema.org/Person + const authorsStructuredData = metadata.authors.map((author) => ({ + '@type': 'Person', + ...(author.name ? {name: author.name} : {}), + ...(author.title ? {description: author.title} : {}), + ...(author.url ? {url: author.url} : {}), + ...(author.email ? {email: author.email} : {}), + ...(author.imageURL ? {image: author.imageURL} : {}), + })); + + const blogUrl = `${siteConfig.url}${metadata.permalink}`; + const imageUrl = image ? withBaseUrl(image, {absolute: true}) : undefined; + + return { + '@type': 'BlogPosting', + '@id': blogUrl, + mainEntityOfPage: blogUrl, + url: blogUrl, + headline: title, + name: title, + description, + datePublished: date, + author: + authorsStructuredData.length === 1 + ? authorsStructuredData[0] + : authorsStructuredData, + ...(image + ? { + // details on structured data support: https://schema.org/ImageObject + image: { + '@type': 'ImageObject', + '@id': imageUrl, + url: imageUrl, + contentUrl: imageUrl, + caption: `title image for the blog post: ${title}`, + }, + } + : {}), + ...(keywords ? {keywords} : {}), + }; + }), + }; + + return ( +