Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(theme): use JSON-LD instead of microdata for blog structured data #9669

Merged
merged 23 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e0da5cf
feat: JSON-LD structured data implementation for blog
johnnyreilly Dec 25, 2023
6521052
Merge branch 'main' of https://github.com/johnnyreilly/docusaurus int…
johnnyreilly Dec 26, 2023
2bc8ca6
fix: tests
johnnyreilly Dec 26, 2023
25afce0
Update packages/docusaurus-theme-classic/src/theme/BlogPostPage/Struc…
johnnyreilly Jan 29, 2024
1df8ce4
Update packages/docusaurus-theme-classic/src/theme/BlogListPage/Struc…
johnnyreilly Jan 29, 2024
7c88ae0
feat: migrate to blogMetadata bundle/prop as suggested by @slorber
johnnyreilly Feb 9, 2024
79224e1
feat: dedicated StructuredData component
johnnyreilly Feb 9, 2024
c21d57a
feat: add structuredDataUtils
johnnyreilly Feb 9, 2024
c5961ea
feat: add schema-dts
johnnyreilly Feb 9, 2024
4cc7a50
fix: single blogMetadata
johnnyreilly Feb 9, 2024
885dbaf
fix: split out getImage
johnnyreilly Feb 10, 2024
6663726
fix: getAuthor / getImage move
johnnyreilly Feb 10, 2024
8ffcb58
fix: getBlogPost
johnnyreilly Feb 10, 2024
ce1b664
fix: baseBlogPermalink -> blogBasePath
johnnyreilly Feb 10, 2024
3ce60a1
fix: migrate structuredData logic to theme-common
johnnyreilly Feb 10, 2024
356d8f2
fix: move StructuredData to theme-common
johnnyreilly Feb 10, 2024
55433eb
fix: remove unnecessary prop
johnnyreilly Feb 10, 2024
e59c882
fix: less prop drilling
johnnyreilly Feb 10, 2024
0be47bd
fix: address review feedback
johnnyreilly Feb 14, 2024
6d26f5c
Add blog plugin useBlogMetadata() client hook + refactor to use it
slorber Feb 14, 2024
84a13d1
fix blog tests???
slorber Feb 14, 2024
20256fa
revert usage of StructuredData comp in docs
slorber Feb 15, 2024
96073e8
remove StructuredData component
slorber Feb 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ describe('blog plugin', () => {
title: 'date-matter',
description: `date inside front matter`,
authors: [],
baseBlogPermalink: '/blog',
baseBlogTitle: 'Blog',
date: new Date('2019-01-01'),
formattedDate: 'January 1, 2019',
frontMatter: {
Expand Down Expand Up @@ -219,6 +221,8 @@ describe('blog plugin', () => {
title: 'Docusaurus maintainer (translated)',
},
],
baseBlogPermalink: '/blog',
baseBlogTitle: 'Blog',
date: new Date('2018-12-14'),
formattedDate: 'December 14, 2018',
frontMatter: {
Expand Down Expand Up @@ -250,6 +254,8 @@ describe('blog plugin', () => {
title: 'Complex Slug',
description: `complex url slug`,
authors: [],
baseBlogPermalink: '/blog',
baseBlogTitle: 'Blog',
prevItem: undefined,
nextItem: {
permalink: '/blog/simple/slug',
Expand Down Expand Up @@ -296,6 +302,8 @@ describe('blog plugin', () => {
imageURL: undefined,
},
],
baseBlogPermalink: '/blog',
baseBlogTitle: 'Blog',
prevItem: undefined,
nextItem: {
permalink: '/blog/draft',
Expand Down Expand Up @@ -327,6 +335,8 @@ describe('blog plugin', () => {
title: 'some heading',
description: '',
authors: [],
baseBlogPermalink: '/blog',
baseBlogTitle: 'Blog',
date: new Date('2019-01-02'),
formattedDate: 'January 2, 2019',
frontMatter: {
Expand Down Expand Up @@ -493,6 +503,8 @@ describe('blog plugin', () => {
title: 'no date',
description: `no date`,
authors: [],
baseBlogPermalink: '/blog',
baseBlogTitle: 'Blog',
date: noDateSourceTime,
formattedDate,
frontMatter: {},
Expand Down
5 changes: 5 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/blogUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ async function processBlogSourceFile(
truncateMarker,
showReadingTime,
editUrl,
blogTitle: baseBlogTitle,
} = options;

// Lookup in localized folder in priority
Expand Down Expand Up @@ -329,6 +330,8 @@ async function processBlogSourceFile(
return undefined;
}

const baseBlogPermalink = normalizeUrl([baseUrl, routeBasePath]);

const tagsBasePath = normalizeUrl([
baseUrl,
routeBasePath,
Expand All @@ -340,6 +343,8 @@ async function processBlogSourceFile(
id: slug,
metadata: {
permalink,
baseBlogPermalink,
baseBlogTitle,
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
editUrl: getBlogEditUrl(),
source: aliasedSource,
title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down Expand Up @@ -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 {
Expand All @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
</main>
{toc && <div className="col col--2">{toc}</div>}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
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 = {
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
'@context': 'https://schema.org',
'@type': 'Blog',
'@id': url,
mainEntityOfPage: url,
headline: blogTitle,
description: blogDescription,
blogPost: props.items.map((blogItem) => {
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
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,
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
...(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 (
<script
type="application/ld+json"
// We're using dangerouslySetInnerHTML because we want to avoid React
// transforming quotes into &quot; which upsets parsers.
// The entire contents is a stringified JSON object so it is safe
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: JSON.stringify(blogStructuredData),
}}
/>
);
}
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +55,7 @@ export default function BlogListPage(props: Props): JSX.Element {
ThemeClassNames.page.blogListPage,
)}>
<BlogListPageMetadata {...props} />
<BlogListPageStructuredData {...props} />
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
<BlogListPageContent {...props} />
</HtmlClassNameProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<article
className={className}
itemProp="blogPost"
itemScope
itemType="https://schema.org/BlogPosting">
{description && <meta itemProp="description" content={description} />}
{image && (
<link itemProp="image" href={withBaseUrl(image, {absolute: true})} />
)}
{keywords.length > 0 && (
<meta itemProp="keywords" content={keywords.join(',')} />
)}
{children}
</article>
);
return <article className={className}>{children}</article>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ export default function BlogPostItemContent({
<div
// This ID is used for the feed generation to locate the main content
id={isBlogPostPage ? blogPostContainerID : undefined}
className={clsx('markdown', className)}
itemProp="articleBody">
className={clsx('markdown', className)}>
<MDXContent>{children}</MDXContent>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,18 @@ export default function BlogPostItemHeaderAuthor({
<div className={clsx('avatar margin-bottom--sm', className)}>
{imageURL && (
<MaybeLink href={link} className="avatar__photo-link">
<img
className="avatar__photo"
src={imageURL}
alt={name}
itemProp="image"
/>
<img className="avatar__photo" src={imageURL} alt={name} />
</MaybeLink>
)}

{name && (
<div
className="avatar__intro"
itemProp="author"
itemScope
itemType="https://schema.org/Person">
<div className="avatar__intro">
<div className="avatar__name">
<MaybeLink href={link} itemProp="url">
<span itemProp="name">{name}</span>
<MaybeLink href={link}>
<span>{name}</span>
</MaybeLink>
</div>
{title && (
<small className="avatar__subtitle" itemProp="description">
{title}
</small>
)}
{title && <small className="avatar__subtitle">{title}</small>}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,7 @@ function ReadingTime({readingTime}: {readingTime: number}) {
}

function Date({date, formattedDate}: {date: string; formattedDate: string}) {
return (
<time dateTime={date} itemProp="datePublished">
{formattedDate}
</time>
);
return <time dateTime={date}>{formattedDate}</time>;
}

function Spacer() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@ export default function BlogPostItemHeaderTitle({
const {permalink, title} = metadata;
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
return (
<TitleHeading className={clsx(styles.title, className)} itemProp="headline">
{isBlogPostPage ? (
title
) : (
<Link itemProp="url" to={permalink}>
{title}
</Link>
)}
<TitleHeading className={clsx(styles.title, className)}>
{isBlogPostPage ? title : <Link to={permalink}>{title}</Link>}
</TitleHeading>
);
}
Loading
Loading