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 (
+
+ );
+}
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx
index 4bb30e8cbf40..563327a8a2a2 100644
--- a/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/BlogListPage/index.tsx
@@ -19,6 +19,7 @@ import BlogListPaginator from '@theme/BlogListPaginator';
import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/BlogListPage';
import BlogPostItems from '@theme/BlogPostItems';
+import BlogListPageStructuredData from '@theme/BlogListPage/StructuredData';
function BlogListPageMetadata(props: Props): JSX.Element {
const {metadata} = props;
@@ -54,6 +55,7 @@ export default function BlogListPage(props: Props): JSX.Element {
ThemeClassNames.page.blogListPage,
)}>
+
);
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Container/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Container/index.tsx
index 05c12d3345ca..5a4fb04e273c 100644
--- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Container/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Container/index.tsx
@@ -6,36 +6,11 @@
*/
import React from 'react';
-import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
-import {useBlogPost} from '@docusaurus/theme-common/internal';
import type {Props} from '@theme/BlogPostItem/Container';
export default function BlogPostItemContainer({
children,
className,
}: Props): JSX.Element {
- const {
- frontMatter,
- assets,
- metadata: {description},
- } = useBlogPost();
- const {withBaseUrl} = useBaseUrlUtils();
- const image = assets.image ?? frontMatter.image;
- const keywords = frontMatter.keywords ?? [];
- return (
-
- {description && }
- {image && (
-
- )}
- {keywords.length > 0 && (
-
- )}
- {children}
-
- );
+ return {children};
}
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Content/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Content/index.tsx
index 592df27d24f6..ebbae6884e67 100644
--- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Content/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Content/index.tsx
@@ -21,8 +21,7 @@ export default function BlogPostItemContent({
+ className={clsx('markdown', className)}>
{children}
);
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx
index 5f2eb1d7be38..5d9935febb04 100644
--- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Author/index.tsx
@@ -28,31 +28,18 @@ export default function BlogPostItemHeaderAuthor({
{imageURL && (
-
+
)}
{name && (
-
+
-
- {name}
+
+ {name}
- {title && (
-
- {title}
-
- )}
+ {title &&
{title}}
)}
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Info/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Info/index.tsx
index 0775884f60de..22811b499ecc 100644
--- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Info/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Info/index.tsx
@@ -40,11 +40,7 @@ function ReadingTime({readingTime}: {readingTime: number}) {
}
function Date({date, formattedDate}: {date: string; formattedDate: string}) {
- return (
-
- );
+ return
;
}
function Spacer() {
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Title/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Title/index.tsx
index ea40c2ec752f..1a6ed4a17253 100644
--- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Title/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/Header/Title/index.tsx
@@ -20,14 +20,8 @@ export default function BlogPostItemHeaderTitle({
const {permalink, title} = metadata;
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
return (
-
- {isBlogPostPage ? (
- title
- ) : (
-
- {title}
-
- )}
+
+ {isBlogPostPage ? title : {title}}
);
}
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostPage/StructuredData/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/StructuredData/index.tsx
new file mode 100644
index 000000000000..3cb53a019a7b
--- /dev/null
+++ b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/StructuredData/index.tsx
@@ -0,0 +1,84 @@
+/**
+ * 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/BlogPostPage/StructuredData';
+
+export default function BlogPostStructuredData(props: Props): JSX.Element {
+ const {siteConfig} = useDocusaurusContext();
+ const {withBaseUrl} = useBaseUrlUtils();
+ const {assets, frontMatter, metadata} = props;
+ 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 url = `${siteConfig.url}${metadata.permalink}`;
+ const imageUrl = image ? withBaseUrl(image, {absolute: true}) : undefined;
+
+ // details on structured data support: https://schema.org/BlogPosting
+ // BlogPosting is one of the structured data types that Google explicitly
+ // supports: https://developers.google.com/search/docs/appearance/structured-data/article#structured-data-type-definitions
+ const blogPostStructuredData = {
+ '@context': 'https://schema.org',
+ '@type': 'BlogPosting',
+ '@id': url,
+ mainEntityOfPage: url,
+ url,
+ 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} : {}),
+ isPartOf: {
+ '@type': 'Blog',
+ '@id': `${siteConfig.url}${metadata.baseBlogPermalink}`,
+ name: metadata.baseBlogTitle,
+ },
+ };
+
+ return (
+
+ );
+}
diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx
index f640f603347b..ea3398f80067 100644
--- a/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/BlogPostPage/index.tsx
@@ -13,6 +13,7 @@ import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
+import BlogPostPageStructuredData from '@theme/BlogPostPage/StructuredData';
import TOC from '@theme/TOC';
import type {Props} from '@theme/BlogPostPage';
import Unlisted from '@theme/Unlisted';
@@ -25,7 +26,7 @@ function BlogPostPageContent({
sidebar: BlogSidebar;
children: ReactNode;
}): JSX.Element {
- const {metadata, toc} = useBlogPost();
+ const {metadata, toc, assets} = useBlogPost();
const {nextItem, prevItem, frontMatter, unlisted} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
@@ -45,6 +46,13 @@ function BlogPostPageContent({
) : undefined
}>
{unlisted && }
+
+
+
{children}
{(nextItem || prevItem) && (
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index 7e11dd04f8af..903e58eac98c 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -430,6 +430,8 @@ module.exports = async function createConfigAsync() {
type: 'all',
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
},
+ blogTitle: 'Docusaurus blog',
+ blogDescription: 'Read blog posts about Docusaurus from the team',
blogSidebarCount: 'ALL',
blogSidebarTitle: 'All our posts',
},