/** * 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 {useBaseUrlUtils, type BaseUrlUtils} from '@docusaurus/useBaseUrl'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import {useBlogMetadata} from '@docusaurus/plugin-content-blog/client'; import type {Props as BlogListPageStructuredDataProps} from '@theme/BlogListPage/StructuredData'; import {useBlogPost} from './contexts'; import type { Blog, BlogPosting, WithContext, Person, ImageObject, } from 'schema-dts'; import type { Author, PropBlogPostContent, } from '@docusaurus/plugin-content-blog'; import type {DocusaurusConfig} from '@docusaurus/types'; const convertDate = (dateMs: number) => new Date(dateMs).toISOString(); function getBlogPost( blogPostContent: PropBlogPostContent, siteConfig: DocusaurusConfig, withBaseUrl: BaseUrlUtils['withBaseUrl'], ): BlogPosting { const {assets, frontMatter, metadata} = blogPostContent; const {date, title, description, lastUpdatedAt} = metadata; const image = assets.image ?? frontMatter.image; const keywords = frontMatter.keywords ?? []; const blogUrl = `${siteConfig.url}${metadata.permalink}`; const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined; return { '@type': 'BlogPosting', '@id': blogUrl, mainEntityOfPage: blogUrl, url: blogUrl, headline: title, name: title, description, datePublished: date, ...(dateModified ? {dateModified} : {}), ...getAuthor(metadata.authors), ...getImage(image, withBaseUrl, title), ...(keywords ? {keywords} : {}), }; } function getAuthor(authors: Author[]) { const authorsStructuredData = authors.map(createPersonStructuredData); return { author: authorsStructuredData.length === 1 ? authorsStructuredData[0] : authorsStructuredData, }; } function getImage( image: string | undefined, withBaseUrl: BaseUrlUtils['withBaseUrl'], title: string, ) { return image ? { image: createImageStructuredData({ imageUrl: withBaseUrl(image, {absolute: true}), caption: `title image for the blog post: ${title}`, }), } : {}; } export function useBlogListPageStructuredData( props: BlogListPageStructuredDataProps, ): WithContext { 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 return { '@context': 'https://schema.org', '@type': 'Blog', '@id': url, mainEntityOfPage: url, headline: blogTitle, description: blogDescription, blogPost: props.items.map((blogItem) => getBlogPost(blogItem.content, siteConfig, withBaseUrl), ), }; } export function useBlogPostStructuredData(): WithContext { const blogMetadata = useBlogMetadata(); const {assets, metadata} = useBlogPost(); const {siteConfig} = useDocusaurusContext(); const {withBaseUrl} = useBaseUrlUtils(); const {date, title, description, frontMatter, lastUpdatedAt} = metadata; const image = assets.image ?? frontMatter.image; const keywords = frontMatter.keywords ?? []; const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined; const url = `${siteConfig.url}${metadata.permalink}`; // 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 return { '@context': 'https://schema.org', '@type': 'BlogPosting', '@id': url, mainEntityOfPage: url, url, headline: title, name: title, description, datePublished: date, ...(dateModified ? {dateModified} : {}), ...getAuthor(metadata.authors), ...getImage(image, withBaseUrl, title), ...(keywords ? {keywords} : {}), isPartOf: { '@type': 'Blog', '@id': `${siteConfig.url}${blogMetadata.blogBasePath}`, name: blogMetadata.blogTitle, }, }; } /** @returns A {@link https://schema.org/Person} constructed from the {@link Author} */ function createPersonStructuredData(author: Author): Person { return { '@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} : {}), }; } /** @returns A {@link https://schema.org/ImageObject} */ function createImageStructuredData({ imageUrl, caption, }: { imageUrl: string; caption: string; }): ImageObject { return { '@type': 'ImageObject', '@id': imageUrl, url: imageUrl, contentUrl: imageUrl, caption, }; }