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 10 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
14 changes: 14 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export default async function pluginContentBlog(
blogArchiveComponent,
routeBasePath,
archiveBasePath,
blogTitle,
} = options;

const {addRoute, createData} = actions;
Expand Down Expand Up @@ -255,6 +256,18 @@ export default async function pluginContentBlog(
),
);

const blogMetadataPath = await createData(
`blogMetadata-${pluginId}.json`,
JSON.stringify(
{
baseBlogPermalink: normalizeUrl([baseUrl, routeBasePath]),
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
blogTitle,
},
null,
2,
),
);

// Create routes for blog entries.
await Promise.all(
blogPosts.map(async (blogPost) => {
Expand All @@ -273,6 +286,7 @@ export default async function pluginContentBlog(
modules: {
sidebar: aliasedSource(sidebarProp),
content: metadata.source,
blogMetadata: aliasedSource(blogMetadataPath),
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,13 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
blogTagsListPath: string;
};

export type BlogMetadata = {
/** the path to the base of the blog */
baseBlogPermalink: string;
/** title of the overall blog */
blogTitle: string;
};

export type BlogTags = {
[permalink: string]: BlogTag;
};
Expand Down Expand Up @@ -532,6 +539,7 @@ declare module '@theme/BlogPostPage' {
BlogPostFrontMatter,
BlogSidebar,
PropBlogPostContent,
BlogMetadata,
} from '@docusaurus/plugin-content-blog';

export type FrontMatter = BlogPostFrontMatter;
Expand All @@ -543,6 +551,8 @@ declare module '@theme/BlogPostPage' {
readonly sidebar: BlogSidebar;
/** Content of this post as an MDX component, with useful metadata. */
readonly content: Content;
/** Metadata about the blog. */
readonly blogMetadata: BlogMetadata;
}

export default function BlogPostPage(props: Props): JSX.Element;
Expand All @@ -552,6 +562,29 @@ declare module '@theme/BlogPostPage/Metadata' {
export default function BlogPostPageMetadata(): JSX.Element;
}

declare module '@theme/BlogPostPage/StructuredData' {
import type {
BlogPostFrontMatter,
PropBlogPostContent,
BlogMetadata,
} 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;
readonly blogMetadata: BlogMetadata;
}

export default function BlogPostStructuredData(props: Props): JSX.Element;
}

declare module '@theme/BlogListPage' {
import type {Content} from '@theme/BlogPostPage';
import type {
Expand All @@ -574,6 +607,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
8 changes: 8 additions & 0 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,14 @@ declare module '@theme/SearchMetadata' {
export default function SearchMetadata(props: Props): JSX.Element;
}

declare module '@theme/StructuredData' {
export interface Props {
readonly structuredData: object;
}

export default function StructuredData(props: Props): JSX.Element;
}

declare module '@theme/LastUpdated' {
export interface Props {
readonly lastUpdatedAt?: number;
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,79 @@
/**
* 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 {
makeImageStructuredData,
makePersonStructuredData,
} from '@docusaurus/theme-common';
import type {Props} from '@theme/BlogListPage/StructuredData';
import StructuredData from '@theme/StructuredData';
import type {Blog, WithContext} from 'schema-dts';

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: WithContext<Blog> = {
'@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 ?? [];

const authorsStructuredData = metadata.authors.map(
makePersonStructuredData,
);

const blogUrl = `${siteConfig.url}${metadata.permalink}`;

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
? {
image: makeImageStructuredData({
imageUrl: withBaseUrl(image, {absolute: true}),
caption: `title image for the blog post: ${title}`,
}),
}
: {}),
johnnyreilly marked this conversation as resolved.
Show resolved Hide resolved
...(keywords ? {keywords} : {}),
};
}),
};

return <StructuredData structuredData={blogStructuredData} />;
}
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