refactor(theme): split BlogPostItem into smaller theme subcomponents (#7716)

This commit is contained in:
Sébastien Lorber 2022-07-08 13:28:53 +02:00 committed by GitHub
parent c7f18801da
commit c3ff131110
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 938 additions and 600 deletions

View file

@ -52,11 +52,11 @@ exports[`translateContent falls back when translation is incomplete 1`] = `
"description": "/blog/2021/06/19/hello", "description": "/blog/2021/06/19/hello",
"formattedDate": "June 19, 2021", "formattedDate": "June 19, 2021",
"frontMatter": {}, "frontMatter": {},
"hasTruncateMarker": true,
"permalink": "/blog/2021/06/19/hello", "permalink": "/blog/2021/06/19/hello",
"source": "/blog/2021/06/19/hello", "source": "/blog/2021/06/19/hello",
"tags": [], "tags": [],
"title": "Hello", "title": "Hello",
"truncated": true,
}, },
}, },
], ],
@ -96,11 +96,11 @@ exports[`translateContent returns translated loaded 1`] = `
"description": "/blog/2021/06/19/hello", "description": "/blog/2021/06/19/hello",
"formattedDate": "June 19, 2021", "formattedDate": "June 19, 2021",
"frontMatter": {}, "frontMatter": {},
"hasTruncateMarker": true,
"permalink": "/blog/2021/06/19/hello", "permalink": "/blog/2021/06/19/hello",
"source": "/blog/2021/06/19/hello", "source": "/blog/2021/06/19/hello",
"tags": [], "tags": [],
"title": "Hello", "title": "Hello",
"truncated": true,
}, },
}, },
], ],

View file

@ -210,7 +210,7 @@ describe('linkify', () => {
permalink: '/blog/2019/01/01/date-matter', permalink: '/blog/2019/01/01/date-matter',
title: 'date-matter', title: 'date-matter',
}, },
truncated: false, hasTruncateMarker: false,
frontMatter: {}, frontMatter: {},
authors: [], authors: [],
formattedDate: '', formattedDate: '',

View file

@ -171,7 +171,7 @@ describe('blog plugin', () => {
permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash', permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash',
title: 'Happy 1st Birthday Slash! (translated)', title: 'Happy 1st Birthday Slash! (translated)',
}, },
truncated: false, hasTruncateMarker: false,
}); });
expect( expect(
@ -214,7 +214,7 @@ describe('blog plugin', () => {
permalink: '/blog/date-matter', permalink: '/blog/date-matter',
title: 'date-matter', title: 'date-matter',
}, },
truncated: false, hasTruncateMarker: false,
}); });
expect({ expect({
@ -251,7 +251,7 @@ describe('blog plugin', () => {
permalink: '/blog/tags/complex', permalink: '/blog/tags/complex',
}, },
], ],
truncated: false, hasTruncateMarker: false,
}); });
expect({ expect({
@ -288,7 +288,7 @@ describe('blog plugin', () => {
title: 'Simple Slug', title: 'Simple Slug',
}, },
tags: [], tags: [],
truncated: false, hasTruncateMarker: false,
}); });
expect({ expect({
@ -313,7 +313,7 @@ describe('blog plugin', () => {
permalink: '/blog/date-matter', permalink: '/blog/date-matter',
title: 'date-matter', title: 'date-matter',
}, },
truncated: false, hasTruncateMarker: false,
}); });
}); });
@ -470,7 +470,7 @@ describe('blog plugin', () => {
tags: [], tags: [],
prevItem: undefined, prevItem: undefined,
nextItem: undefined, nextItem: undefined,
truncated: false, hasTruncateMarker: false,
}); });
}); });

View file

@ -32,7 +32,7 @@ const sampleBlogPosts: BlogPost[] = [
formattedDate: 'June 19, 2021', formattedDate: 'June 19, 2021',
tags: [], tags: [],
title: 'Hello', title: 'Hello',
truncated: true, hasTruncateMarker: true,
authors: [], authors: [],
frontMatter: {}, frontMatter: {},
}, },

View file

@ -323,7 +323,7 @@ async function processBlogSourceFile(
defaultReadingTime, defaultReadingTime,
}) })
: undefined, : undefined,
truncated: truncateMarker.test(content), hasTruncateMarker: truncateMarker.test(content),
authors, authors,
frontMatter, frontMatter,
}, },

View file

@ -195,6 +195,21 @@ export default async function pluginContentBlog(
? blogPosts ? blogPosts
: blogPosts.slice(0, options.blogSidebarCount); : blogPosts.slice(0, options.blogSidebarCount);
function blogPostItemsModule(items: string[]) {
return items.map((postId) => {
const blogPostMetadata = blogItemsToMetadata[postId]!;
return {
content: {
__import: true,
path: blogPostMetadata.source,
query: {
truncated: true,
},
},
};
});
}
if (archiveBasePath && blogPosts.length) { if (archiveBasePath && blogPosts.length) {
const archiveUrl = normalizeUrl([ const archiveUrl = normalizeUrl([
baseUrl, baseUrl,
@ -275,15 +290,7 @@ export default async function pluginContentBlog(
exact: true, exact: true,
modules: { modules: {
sidebar: aliasedSource(sidebarProp), sidebar: aliasedSource(sidebarProp),
items: items.map((postID) => ({ items: blogPostItemsModule(items),
content: {
__import: true,
path: blogItemsToMetadata[postID]!.source,
query: {
truncated: true,
},
},
})),
metadata: aliasedSource(pageMetadataPath), metadata: aliasedSource(pageMetadataPath),
}, },
}); });
@ -344,18 +351,7 @@ export default async function pluginContentBlog(
exact: true, exact: true,
modules: { modules: {
sidebar: aliasedSource(sidebarProp), sidebar: aliasedSource(sidebarProp),
items: items.map((postID) => { items: blogPostItemsModule(items),
const blogPostMetadata = blogItemsToMetadata[postID]!;
return {
content: {
__import: true,
path: blogPostMetadata.source,
query: {
truncated: true,
},
},
};
}),
tag: aliasedSource(tagPropPath), tag: aliasedSource(tagPropPath),
listMetadata: aliasedSource(listMetadataPath), listMetadata: aliasedSource(listMetadataPath),
}, },

View file

@ -6,6 +6,7 @@
*/ */
declare module '@docusaurus/plugin-content-blog' { declare module '@docusaurus/plugin-content-blog' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {MDXOptions} from '@docusaurus/mdx-loader'; import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {FrontMatterTag, Tag} from '@docusaurus/utils'; import type {FrontMatterTag, Tag} from '@docusaurus/utils';
import type {Plugin, LoadContext} from '@docusaurus/types'; import type {Plugin, LoadContext} from '@docusaurus/types';
@ -201,7 +202,7 @@ declare module '@docusaurus/plugin-content-blog' {
/** /**
* Whether the truncate marker exists in the post's content. * Whether the truncate marker exists in the post's content.
*/ */
readonly truncated?: boolean; readonly hasTruncateMarker: boolean;
/** /**
* Used in pagination. Generated after the other metadata, so not readonly. * Used in pagination. Generated after the other metadata, so not readonly.
* Content is just a subset of another post's metadata. * Content is just a subset of another post's metadata.
@ -462,25 +463,7 @@ declare module '@docusaurus/plugin-content-blog' {
items: string[]; items: string[];
}; };
export default function pluginContentBlog( type PropBlogPostMetadata = Overwrite<
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<BlogContent>>;
}
declare module '@theme/BlogPostPage' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {
BlogPostFrontMatter,
BlogPostMetadata,
Assets,
BlogSidebar,
} from '@docusaurus/plugin-content-blog';
import type {Overwrite} from 'utility-types';
export type FrontMatter = BlogPostFrontMatter;
export type Metadata = Overwrite<
BlogPostMetadata, BlogPostMetadata,
{ {
/** The publish date of the post. Serialized from the `Date` object. */ /** The publish date of the post. Serialized from the `Date` object. */
@ -488,7 +471,28 @@ declare module '@theme/BlogPostPage' {
} }
>; >;
export type Content = LoadedMDXContent<FrontMatter, Metadata, Assets>; export type PropBlogPostContent = LoadedMDXContent<
BlogPostFrontMatter,
PropBlogPostMetadata,
Assets
>;
export default function pluginContentBlog(
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<BlogContent>>;
}
declare module '@theme/BlogPostPage' {
import type {
BlogPostFrontMatter,
BlogSidebar,
PropBlogPostContent,
} from '@docusaurus/plugin-content-blog';
export type FrontMatter = BlogPostFrontMatter;
export type Content = PropBlogPostContent;
export interface Props { export interface Props {
/** Blog sidebar. */ /** Blog sidebar. */
@ -500,6 +504,10 @@ declare module '@theme/BlogPostPage' {
export default function BlogPostPage(props: Props): JSX.Element; export default function BlogPostPage(props: Props): JSX.Element;
} }
declare module '@theme/BlogPostPage/Metadata' {
export default function BlogPostPageMetadata(): JSX.Element;
}
declare module '@theme/BlogListPage' { declare module '@theme/BlogListPage' {
import type {Content} from '@theme/BlogPostPage'; import type {Content} from '@theme/BlogPostPage';
import type { import type {

View file

@ -95,41 +95,103 @@ declare module '@theme/BlogSidebar' {
} }
declare module '@theme/BlogPostItem' { declare module '@theme/BlogPostItem' {
import type {FrontMatter, Metadata} from '@theme/BlogPostPage'; import type {ReactNode} from 'react';
import type {Assets} from '@docusaurus/plugin-content-blog';
export interface Props { export interface Props {
readonly frontMatter: FrontMatter; children: ReactNode;
readonly assets: Assets; className?: string;
readonly metadata: Metadata;
readonly truncated?: string | boolean;
readonly isBlogPostPage?: boolean;
readonly children: JSX.Element;
} }
export default function BlogPostItem(props: Props): JSX.Element; export default function BlogPostItem(props: Props): JSX.Element;
} }
declare module '@theme/BlogPostAuthor' { declare module '@theme/BlogPostItems' {
import type {Metadata} from '@theme/BlogPostPage'; import type {ComponentType, ReactNode} from 'react';
import type {PropBlogPostContent} from '@docusaurus/plugin-content-blog';
export interface Props { export interface Props {
readonly author: Metadata['authors'][number]; items: readonly {content: PropBlogPostContent}[];
component?: ComponentType<{children: ReactNode}>;
} }
export default function BlogPostAuthor(props: Props): JSX.Element; export default function BlogPostItem(props: Props): JSX.Element;
} }
declare module '@theme/BlogPostAuthors' { declare module '@theme/BlogPostItem/Container' {
import type {Metadata} from '@theme/BlogPostPage'; import type {ReactNode} from 'react';
import type {Assets} from '@docusaurus/plugin-content-blog';
export interface Props { export interface Props {
readonly authors: Metadata['authors']; children: ReactNode;
readonly assets: Assets; className?: string;
} }
export default function BlogPostAuthors(props: Props): JSX.Element; export default function BlogPostItemContainer(props: Props): JSX.Element;
}
declare module '@theme/BlogPostItem/Header' {
export default function BlogPostItemHeader(): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Title' {
export interface Props {
className?: string;
}
export default function BlogPostItemHeaderTitle(props: Props): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Info' {
export interface Props {
className?: string;
}
export default function BlogPostItemHeaderInfo(): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Author' {
import type {PropBlogPostContent} from '@docusaurus/plugin-content-blog';
export interface Props {
readonly author: PropBlogPostContent['metadata']['authors'][number];
readonly className?: string;
}
export default function BlogPostItemHeaderAuthor(props: Props): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Authors' {
export interface Props {
readonly className?: string;
}
export default function BlogPostItemHeaderAuthors(props: Props): JSX.Element;
}
declare module '@theme/BlogPostItem/Content' {
import type {ReactNode} from 'react';
export interface Props {
children: ReactNode;
className?: string;
}
export default function BlogPostItemContent(props: Props): JSX.Element;
}
declare module '@theme/BlogPostItem/Footer' {
export default function BlogPostItemFooter(): JSX.Element | null;
}
declare module '@theme/BlogPostItem/Footer/ReadMoreLink' {
import type {Props as LinkProps} from '@docusaurus/Link';
export type Props = LinkProps & {
blogPostTitle: string;
};
export default function BlogPostItemFooterReadMoreLink(
props: Props,
): JSX.Element | null;
} }
declare module '@theme/BlogPostPaginator' { declare module '@theme/BlogPostPaginator' {

View file

@ -15,10 +15,10 @@ import {
ThemeClassNames, ThemeClassNames,
} from '@docusaurus/theme-common'; } from '@docusaurus/theme-common';
import BlogLayout from '@theme/BlogLayout'; import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogListPaginator from '@theme/BlogListPaginator'; import BlogListPaginator from '@theme/BlogListPaginator';
import SearchMetadata from '@theme/SearchMetadata'; import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/BlogListPage'; import type {Props} from '@theme/BlogListPage';
import BlogPostItems from '@theme/BlogPostItems';
function BlogListPageMetadata(props: Props): JSX.Element { function BlogListPageMetadata(props: Props): JSX.Element {
const {metadata} = props; const {metadata} = props;
@ -40,16 +40,7 @@ function BlogListPageContent(props: Props): JSX.Element {
const {metadata, items, sidebar} = props; const {metadata, items, sidebar} = props;
return ( return (
<BlogLayout sidebar={sidebar}> <BlogLayout sidebar={sidebar}>
{items.map(({content: BlogPostContent}) => ( <BlogPostItems items={items} />
<BlogPostItem
key={BlogPostContent.metadata.permalink}
frontMatter={BlogPostContent.frontMatter}
assets={BlogPostContent.assets}
metadata={BlogPostContent.metadata}
truncated={BlogPostContent.metadata.truncated}>
<BlogPostContent />
</BlogPostItem>
))}
<BlogListPaginator metadata={metadata} /> <BlogListPaginator metadata={metadata} />
</BlogLayout> </BlogLayout>
); );

View file

@ -0,0 +1,32 @@
/**
* 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 {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} = useBlogPost();
const {withBaseUrl} = useBaseUrlUtils();
const image = assets.image ?? frontMatter.image;
return (
<article
className={className}
itemProp="blogPost"
itemScope
itemType="http://schema.org/BlogPosting">
{image && (
<meta itemProp="image" content={withBaseUrl(image, {absolute: true})} />
)}
{children}
</article>
);
}

View file

@ -0,0 +1,29 @@
/**
* 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 clsx from 'clsx';
import {blogPostContainerID} from '@docusaurus/utils-common';
import {useBlogPost} from '@docusaurus/theme-common/internal';
import MDXContent from '@theme/MDXContent';
import type {Props} from '@theme/BlogPostItem/Content';
export default function BlogPostItemContent({
children,
className,
}: Props): JSX.Element {
const {isBlogPostPage} = useBlogPost();
return (
<div
// This ID is used for the feed generation to locate the main content
id={isBlogPostPage ? blogPostContainerID : undefined}
className={clsx('markdown', className)}
itemProp="articleBody">
<MDXContent>{children}</MDXContent>
</div>
);
}

View file

@ -0,0 +1,44 @@
/**
* 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 Translate, {translate} from '@docusaurus/Translate';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogPostItem/Footer/ReadMoreLink';
function ReadMoreLabel() {
return (
<b>
<Translate
id="theme.blog.post.readMore"
description="The label used in blog post item excerpts to link to full blog posts">
Read More
</Translate>
</b>
);
}
export default function BlogPostItemFooterReadMoreLink(
props: Props,
): JSX.Element {
const {blogPostTitle, ...linkProps} = props;
return (
<Link
aria-label={translate(
{
message: 'Read more about {title}',
id: 'theme.blog.post.readMoreLabel',
description:
'The ARIA label for the link to full blog posts from excerpts',
},
{title: blogPostTitle},
)}
{...linkProps}>
<ReadMoreLabel />
</Link>
);
}

View file

@ -0,0 +1,60 @@
/**
* 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 clsx from 'clsx';
import {useBlogPost} from '@docusaurus/theme-common/internal';
import EditThisPage from '@theme/EditThisPage';
import TagsListInline from '@theme/TagsListInline';
import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink';
import styles from './styles.module.css';
export default function BlogPostItemFooter(): JSX.Element | null {
const {metadata, isBlogPostPage} = useBlogPost();
const {tags, title, editUrl, hasTruncateMarker} = metadata;
// A post is truncated if it's in the "list view" and it has a truncate marker
const truncatedPost = !isBlogPostPage && hasTruncateMarker;
const tagsExists = tags.length > 0;
const renderFooter = tagsExists || truncatedPost || editUrl;
if (!renderFooter) {
return null;
}
return (
<footer
className={clsx(
'row docusaurus-mt-lg',
isBlogPostPage && styles.blogPostFooterDetailsFull,
)}>
{tagsExists && (
<div className={clsx('col', {'col--9': truncatedPost})}>
<TagsListInline tags={tags} />
</div>
)}
{isBlogPostPage && editUrl && (
<div className="col margin-top--sm">
<EditThisPage editUrl={editUrl} />
</div>
)}
{truncatedPost && (
<div
className={clsx('col text--right', {
'col--3': tagsExists,
})}>
<ReadMoreLink blogPostTitle={title} to={metadata.permalink} />
</div>
)}
</footer>
);
}

View file

@ -0,0 +1,10 @@
/**
* 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.
*/
.blogPostFooterDetailsFull {
flex-direction: column;
}

View file

@ -6,8 +6,10 @@
*/ */
import React from 'react'; import React from 'react';
import clsx from 'clsx';
import Link, {type Props as LinkProps} from '@docusaurus/Link'; import Link, {type Props as LinkProps} from '@docusaurus/Link';
import type {Props} from '@theme/BlogPostAuthor';
import type {Props} from '@theme/BlogPostItem/Header/Author';
function MaybeLink(props: LinkProps): JSX.Element { function MaybeLink(props: LinkProps): JSX.Element {
if (props.href) { if (props.href) {
@ -16,11 +18,14 @@ function MaybeLink(props: LinkProps): JSX.Element {
return <>{props.children}</>; return <>{props.children}</>;
} }
export default function BlogPostAuthor({author}: Props): JSX.Element { export default function BlogPostItemHeaderAuthor({
author,
className,
}: Props): JSX.Element {
const {name, title, url, imageURL, email} = author; const {name, title, url, imageURL, email} = author;
const link = url || (email && `mailto:${email}`) || undefined; const link = url || (email && `mailto:${email}`) || undefined;
return ( return (
<div className="avatar margin-bottom--sm"> <div className={clsx('avatar margin-bottom--sm', className)}>
{imageURL && ( {imageURL && (
<MaybeLink href={link} className="avatar__photo-link"> <MaybeLink href={link} className="avatar__photo-link">
<img className="avatar__photo" src={imageURL} alt={name} /> <img className="avatar__photo" src={imageURL} alt={name} />

View file

@ -7,16 +7,19 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import BlogPostAuthor from '@theme/BlogPostAuthor'; import {useBlogPost} from '@docusaurus/theme-common/internal';
import type {Props} from '@theme/BlogPostAuthors'; import BlogPostItemHeaderAuthor from '@theme/BlogPostItem/Header/Author';
import type {Props} from '@theme/BlogPostItem/Header/Authors';
import styles from './styles.module.css'; import styles from './styles.module.css';
// Component responsible for the authors layout // Component responsible for the authors layout
export default function BlogPostAuthors({ export default function BlogPostItemHeaderAuthors({
authors, className,
assets,
}: Props): JSX.Element | null { }: Props): JSX.Element | null {
const {
metadata: {authors},
assets,
} = useBlogPost();
const authorsCount = authors.length; const authorsCount = authors.length;
if (authorsCount === 0) { if (authorsCount === 0) {
return null; return null;
@ -27,6 +30,7 @@ export default function BlogPostAuthors({
className={clsx( className={clsx(
'margin-top--md margin-bottom--sm', 'margin-top--md margin-bottom--sm',
imageOnly ? styles.imageOnlyAuthorRow : 'row', imageOnly ? styles.imageOnlyAuthorRow : 'row',
className,
)}> )}>
{authors.map((author, idx) => ( {authors.map((author, idx) => (
<div <div
@ -35,7 +39,7 @@ export default function BlogPostAuthors({
imageOnly ? styles.imageOnlyAuthorCol : styles.authorCol, imageOnly ? styles.imageOnlyAuthorCol : styles.authorCol,
)} )}
key={idx}> key={idx}>
<BlogPostAuthor <BlogPostItemHeaderAuthor
author={{ author={{
...author, ...author,
// Handle author images using relative paths // Handle author images using relative paths

View file

@ -0,0 +1,71 @@
/**
* 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 clsx from 'clsx';
import {translate} from '@docusaurus/Translate';
import {usePluralForm} from '@docusaurus/theme-common';
import {useBlogPost} from '@docusaurus/theme-common/internal';
import type {Props} from '@theme/BlogPostItem/Header/Info';
import styles from './styles.module.css';
// Very simple pluralization: probably good enough for now
function useReadingTimePlural() {
const {selectMessage} = usePluralForm();
return (readingTimeFloat: number) => {
const readingTime = Math.ceil(readingTimeFloat);
return selectMessage(
readingTime,
translate(
{
id: 'theme.blog.post.readingTime.plurals',
description:
'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
message: 'One min read|{readingTime} min read',
},
{readingTime},
),
);
};
}
function ReadingTime({readingTime}: {readingTime: number}) {
const readingTimePlural = useReadingTimePlural();
return <>{readingTimePlural(readingTime)}</>;
}
function Date({date, formattedDate}: {date: string; formattedDate: string}) {
return (
<time dateTime={date} itemProp="datePublished">
{formattedDate}
</time>
);
}
function Spacer() {
return <>{' · '}</>;
}
export default function BlogPostItemHeaderInfo({
className,
}: Props): JSX.Element {
const {metadata} = useBlogPost();
const {date, formattedDate, readingTime} = metadata;
return (
<div className={clsx(styles.container, 'margin-vert--md', className)}>
<Date date={date} formattedDate={formattedDate} />
{typeof readingTime !== 'undefined' && (
<>
<Spacer />
<ReadingTime readingTime={readingTime} />
</>
)}
</div>
);
}

View file

@ -0,0 +1,10 @@
/**
* 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.
*/
.container {
font-size: 0.9rem;
}

View file

@ -0,0 +1,33 @@
/**
* 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 clsx from 'clsx';
import Link from '@docusaurus/Link';
import {useBlogPost} from '@docusaurus/theme-common/internal';
import type {Props} from '@theme/BlogPostItem/Header/Title';
import styles from './styles.module.css';
export default function BlogPostItemHeaderTitle({
className,
}: Props): JSX.Element {
const {metadata, isBlogPostPage} = useBlogPost();
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>
);
}

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
.blogPostTitle { .title {
font-size: 3rem; font-size: 3rem;
} }
@ -13,15 +13,7 @@
Blog post title should be smaller on smaller devices Blog post title should be smaller on smaller devices
**/ **/
@media (max-width: 576px) { @media (max-width: 576px) {
.blogPostTitle { .title {
font-size: 2rem; font-size: 2rem;
} }
} }
.blogPostData {
font-size: 0.9rem;
}
.blogPostDetailsFull {
flex-direction: column;
}

View file

@ -0,0 +1,21 @@
/**
* 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 BlogPostItemHeaderTitle from '@theme/BlogPostItem/Header/Title';
import BlogPostItemHeaderInfo from '@theme/BlogPostItem/Header/Info';
import BlogPostItemHeaderAuthors from '@theme/BlogPostItem/Header/Authors';
export default function BlogPostItemHeader(): JSX.Element {
return (
<header>
<BlogPostItemHeaderTitle />
<BlogPostItemHeaderInfo />
<BlogPostItemHeaderAuthors />
</header>
);
}

View file

@ -7,155 +7,29 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import Translate, {translate} from '@docusaurus/Translate'; import {useBlogPost} from '@docusaurus/theme-common/internal';
import Link from '@docusaurus/Link'; import BlogPostItemContainer from '@theme/BlogPostItem/Container';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl'; import BlogPostItemHeader from '@theme/BlogPostItem/Header';
import {usePluralForm} from '@docusaurus/theme-common'; import BlogPostItemContent from '@theme/BlogPostItem/Content';
import {blogPostContainerID} from '@docusaurus/utils-common'; import BlogPostItemFooter from '@theme/BlogPostItem/Footer';
import MDXContent from '@theme/MDXContent';
import EditThisPage from '@theme/EditThisPage';
import TagsListInline from '@theme/TagsListInline';
import BlogPostAuthors from '@theme/BlogPostAuthors';
import type {Props} from '@theme/BlogPostItem'; import type {Props} from '@theme/BlogPostItem';
import styles from './styles.module.css'; // apply a bottom margin in list view
function useContainerClassName() {
// Very simple pluralization: probably good enough for now const {isBlogPostPage} = useBlogPost();
function useReadingTimePlural() { return !isBlogPostPage ? 'margin-bottom--xl' : undefined;
const {selectMessage} = usePluralForm();
return (readingTimeFloat: number) => {
const readingTime = Math.ceil(readingTimeFloat);
return selectMessage(
readingTime,
translate(
{
id: 'theme.blog.post.readingTime.plurals',
description:
'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
message: 'One min read|{readingTime} min read',
},
{readingTime},
),
);
};
} }
export default function BlogPostItem(props: Props): JSX.Element { export default function BlogPostItem({
const readingTimePlural = useReadingTimePlural(); children,
const {withBaseUrl} = useBaseUrlUtils(); className,
const { }: Props): JSX.Element {
children, const containerClassName = useContainerClassName();
frontMatter,
assets,
metadata,
truncated,
isBlogPostPage = false,
} = props;
const {
date,
formattedDate,
permalink,
tags,
readingTime,
title,
editUrl,
authors,
} = metadata;
const image = assets.image ?? frontMatter.image;
const truncatedPost = !isBlogPostPage && truncated;
const tagsExists = tags.length > 0;
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
return ( return (
<article <BlogPostItemContainer className={clsx(containerClassName, className)}>
className={!isBlogPostPage ? 'margin-bottom--xl' : undefined} <BlogPostItemHeader />
itemProp="blogPost" <BlogPostItemContent>{children}</BlogPostItemContent>
itemScope <BlogPostItemFooter />
itemType="http://schema.org/BlogPosting"> </BlogPostItemContainer>
<header>
<TitleHeading className={styles.blogPostTitle} itemProp="headline">
{isBlogPostPage ? (
title
) : (
<Link itemProp="url" to={permalink}>
{title}
</Link>
)}
</TitleHeading>
<div className={clsx(styles.blogPostData, 'margin-vert--md')}>
<time dateTime={date} itemProp="datePublished">
{formattedDate}
</time>
{typeof readingTime !== 'undefined' && (
<>
{' · '}
{readingTimePlural(readingTime)}
</>
)}
</div>
<BlogPostAuthors authors={authors} assets={assets} />
</header>
{image && (
<meta itemProp="image" content={withBaseUrl(image, {absolute: true})} />
)}
<div
// This ID is used for the feed generation to locate the main content
id={isBlogPostPage ? blogPostContainerID : undefined}
className="markdown"
itemProp="articleBody">
<MDXContent>{children}</MDXContent>
</div>
{(tagsExists || truncated || editUrl) && (
<footer
className={clsx(
'row docusaurus-mt-lg',
isBlogPostPage && styles.blogPostDetailsFull,
)}>
{tagsExists && (
<div className={clsx('col', {'col--9': truncatedPost})}>
<TagsListInline tags={tags} />
</div>
)}
{isBlogPostPage && editUrl && (
<div className="col margin-top--sm">
<EditThisPage editUrl={editUrl} />
</div>
)}
{truncatedPost && (
<div
className={clsx('col text--right', {
'col--3': tagsExists,
})}>
<Link
to={metadata.permalink}
aria-label={translate(
{
message: 'Read more about {title}',
id: 'theme.blog.post.readMoreLabel',
description:
'The ARIA label for the link to full blog posts from excerpts',
},
{title},
)}>
<b>
<Translate
id="theme.blog.post.readMore"
description="The label used in blog post item excerpts to link to full blog posts">
Read More
</Translate>
</b>
</Link>
</div>
)}
</footer>
)}
</article>
); );
} }

View file

@ -0,0 +1,30 @@
/**
* 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 {BlogPostProvider} from '@docusaurus/theme-common/internal';
import BlogPostItem from '@theme/BlogPostItem';
import type {Props} from '@theme/BlogPostItems';
export default function BlogPostItems({
items,
component: BlogPostItemComponent = BlogPostItem,
}: Props): JSX.Element {
return (
<>
{items.map(({content: BlogPostContent}) => (
<BlogPostProvider
key={BlogPostContent.metadata.permalink}
content={BlogPostContent}>
<BlogPostItemComponent>
<BlogPostContent />
</BlogPostItemComponent>
</BlogPostProvider>
))}
</>
);
}

View file

@ -0,0 +1,44 @@
/**
* 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 {PageMetadata} from '@docusaurus/theme-common';
import {useBlogPost} from '@docusaurus/theme-common/internal';
export default function BlogPostPageMetadata(): JSX.Element {
const {assets, metadata} = useBlogPost();
const {title, description, date, tags, authors, frontMatter} = metadata;
const {keywords} = frontMatter;
const image = assets.image ?? frontMatter.image;
return (
<PageMetadata
title={title}
description={description}
keywords={keywords}
image={image}>
<meta property="og:type" content="article" />
<meta property="article:published_time" content={date} />
{/* TODO double check those article meta array syntaxes, see https://ogp.me/#array */}
{authors.some((author) => author.url) && (
<meta
property="article:author"
content={authors
.map((author) => author.url)
.filter(Boolean)
.join(',')}
/>
)}
{tags.length > 0 && (
<meta
property="article:tag"
content={tags.map((tag) => tag.label).join(',')}
/>
)}
</PageMetadata>
);
}

View file

@ -5,56 +5,26 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react'; import React, {type ReactNode} from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common';
PageMetadata, import {BlogPostProvider, useBlogPost} from '@docusaurus/theme-common/internal';
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import BlogLayout from '@theme/BlogLayout'; import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem'; import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator'; import BlogPostPaginator from '@theme/BlogPostPaginator';
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
import TOC from '@theme/TOC'; import TOC from '@theme/TOC';
import type {Props} from '@theme/BlogPostPage'; import type {Props} from '@theme/BlogPostPage';
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
function BlogPostPageMetadata(props: Props): JSX.Element { function BlogPostPageContent({
const {content: BlogPostContents} = props; sidebar,
const {assets, metadata} = BlogPostContents; children,
const {title, description, date, tags, authors, frontMatter} = metadata; }: {
const {keywords} = frontMatter; sidebar: BlogSidebar;
const image = assets.image ?? frontMatter.image; children: ReactNode;
return ( }): JSX.Element {
<PageMetadata const {metadata, toc} = useBlogPost();
title={title}
description={description}
keywords={keywords}
image={image}>
<meta property="og:type" content="article" />
<meta property="article:published_time" content={date} />
{/* TODO double check those article meta array syntaxes, see https://ogp.me/#array */}
{authors.some((author) => author.url) && (
<meta
property="article:author"
content={authors
.map((author) => author.url)
.filter(Boolean)
.join(',')}
/>
)}
{tags.length > 0 && (
<meta
property="article:tag"
content={tags.map((tag) => tag.label).join(',')}
/>
)}
</PageMetadata>
);
}
function BlogPostPageContent(props: Props): JSX.Element {
const {content: BlogPostContents, sidebar} = props;
const {assets, metadata} = BlogPostContents;
const {nextItem, prevItem, frontMatter} = metadata; const {nextItem, prevItem, frontMatter} = metadata;
const { const {
hide_table_of_contents: hideTableOfContents, hide_table_of_contents: hideTableOfContents,
@ -65,21 +35,15 @@ function BlogPostPageContent(props: Props): JSX.Element {
<BlogLayout <BlogLayout
sidebar={sidebar} sidebar={sidebar}
toc={ toc={
!hideTableOfContents && BlogPostContents.toc.length > 0 ? ( !hideTableOfContents && toc.length > 0 ? (
<TOC <TOC
toc={BlogPostContents.toc} toc={toc}
minHeadingLevel={tocMinHeadingLevel} minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel} maxHeadingLevel={tocMaxHeadingLevel}
/> />
) : undefined ) : undefined
}> }>
<BlogPostItem <BlogPostItem>{children}</BlogPostItem>
frontMatter={frontMatter}
assets={assets}
metadata={metadata}
isBlogPostPage>
<BlogPostContents />
</BlogPostItem>
{(nextItem || prevItem) && ( {(nextItem || prevItem) && (
<BlogPostPaginator nextItem={nextItem} prevItem={prevItem} /> <BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />
@ -89,14 +53,19 @@ function BlogPostPageContent(props: Props): JSX.Element {
} }
export default function BlogPostPage(props: Props): JSX.Element { export default function BlogPostPage(props: Props): JSX.Element {
const BlogPostContent = props.content;
return ( return (
<HtmlClassNameProvider <BlogPostProvider content={props.content} isBlogPostPage>
className={clsx( <HtmlClassNameProvider
ThemeClassNames.wrapper.blogPages, className={clsx(
ThemeClassNames.page.blogPostPage, ThemeClassNames.wrapper.blogPages,
)}> ThemeClassNames.page.blogPostPage,
<BlogPostPageMetadata {...props} /> )}>
<BlogPostPageContent {...props} /> <BlogPostPageMetadata />
</HtmlClassNameProvider> <BlogPostPageContent sidebar={props.sidebar}>
<BlogPostContent />
</BlogPostPageContent>
</HtmlClassNameProvider>
</BlogPostProvider>
); );
} }

View file

@ -16,10 +16,10 @@ import {
} from '@docusaurus/theme-common'; } from '@docusaurus/theme-common';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import BlogLayout from '@theme/BlogLayout'; import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogListPaginator from '@theme/BlogListPaginator'; import BlogListPaginator from '@theme/BlogListPaginator';
import SearchMetadata from '@theme/SearchMetadata'; import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/BlogTagsPostsPage'; import type {Props} from '@theme/BlogTagsPostsPage';
import BlogPostItems from '@theme/BlogPostItems';
// Very simple pluralization: probably good enough for now // Very simple pluralization: probably good enough for now
function useBlogPostsPlural() { function useBlogPostsPlural() {
@ -39,14 +39,9 @@ function useBlogPostsPlural() {
); );
} }
export default function BlogTagsPostsPage({ function useBlogTagsPostsPageTitle(tag: Props['tag']): string {
tag,
items,
sidebar,
listMetadata,
}: Props): JSX.Element {
const blogPostsPlural = useBlogPostsPlural(); const blogPostsPlural = useBlogPostsPlural();
const title = translate( return translate(
{ {
id: 'theme.blog.tagTitle', id: 'theme.blog.tagTitle',
description: 'The title of the page for a blog tag', description: 'The title of the page for a blog tag',
@ -54,40 +49,52 @@ export default function BlogTagsPostsPage({
}, },
{nPosts: blogPostsPlural(tag.count), tagName: tag.label}, {nPosts: blogPostsPlural(tag.count), tagName: tag.label},
); );
}
function BlogTagsPostsPageMetadata({tag}: Props): JSX.Element {
const title = useBlogTagsPostsPageTitle(tag);
return (
<>
<PageMetadata title={title} />
<SearchMetadata tag="blog_tags_posts" />
</>
);
}
function BlogTagsPostsPageContent({
tag,
items,
sidebar,
listMetadata,
}: Props): JSX.Element {
const title = useBlogTagsPostsPageTitle(tag);
return (
<BlogLayout sidebar={sidebar}>
<header className="margin-bottom--xl">
<h1>{title}</h1>
<Link href={tag.allTagsPath}>
<Translate
id="theme.tags.tagsPageLink"
description="The label of the link targeting the tag list page">
View All Tags
</Translate>
</Link>
</header>
<BlogPostItems items={items} />
<BlogListPaginator metadata={listMetadata} />
</BlogLayout>
);
}
export default function BlogTagsPostsPage(props: Props): JSX.Element {
return ( return (
<HtmlClassNameProvider <HtmlClassNameProvider
className={clsx( className={clsx(
ThemeClassNames.wrapper.blogPages, ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogTagPostListPage, ThemeClassNames.page.blogTagPostListPage,
)}> )}>
<PageMetadata title={title} /> <BlogTagsPostsPageMetadata {...props} />
<SearchMetadata tag="blog_tags_posts" /> <BlogTagsPostsPageContent {...props} />
<BlogLayout sidebar={sidebar}>
<header className="margin-bottom--xl">
<h1>{title}</h1>
<Link href={tag.allTagsPath}>
<Translate
id="theme.tags.tagsPageLink"
description="The label of the link targeting the tag list page">
View All Tags
</Translate>
</Link>
</header>
{items.map(({content: BlogPostContent}) => (
<BlogPostItem
key={BlogPostContent.metadata.permalink}
frontMatter={BlogPostContent.frontMatter}
assets={BlogPostContent.assets}
metadata={BlogPostContent.metadata}
truncated>
<BlogPostContent />
</BlogPostItem>
))}
<BlogListPaginator metadata={listMetadata} />
</BlogLayout>
</HtmlClassNameProvider> </HtmlClassNameProvider>
); );
} }

View file

@ -0,0 +1,80 @@
/**
* 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, {useMemo, type ReactNode, useContext} from 'react';
import {ReactContextError} from '../utils/reactUtils';
import type {PropBlogPostContent} from '@docusaurus/plugin-content-blog';
/**
* The React context value returned by the `useBlogPost()` hook.
* It contains useful data related to the currently browsed blog post.
*/
export type BlogPostContextValue = Pick<
PropBlogPostContent,
'metadata' | 'frontMatter' | 'assets' | 'toc'
> & {
readonly isBlogPostPage: boolean;
};
const Context = React.createContext<BlogPostContextValue | null>(null);
/**
* Note: we don't use `PropBlogPostContent` as context value on purpose. Metadata is
* currently stored inside the MDX component, but we may want to change that in
* the future.
*/
function useContextValue({
content,
isBlogPostPage,
}: {
content: PropBlogPostContent;
isBlogPostPage: boolean;
}): BlogPostContextValue {
return useMemo(
() => ({
metadata: content.metadata,
frontMatter: content.frontMatter,
assets: content.assets,
toc: content.toc,
isBlogPostPage,
}),
[content, isBlogPostPage],
);
}
/**
* This is a very thin layer around the `content` received from the MDX loader.
* It provides metadata about the blog post to the children tree.
*/
export function BlogPostProvider({
children,
content,
isBlogPostPage = false,
}: {
children: ReactNode;
content: PropBlogPostContent;
isBlogPostPage?: boolean;
}): JSX.Element {
const contextValue = useContextValue({content, isBlogPostPage});
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
/**
* Returns the data of the currently browsed blog post. Gives access to
* front matter, metadata, TOC, etc.
* When swizzling a low-level component (e.g. the "Edit this page" link)
* and you need some extra metadata, you don't have to drill the props
* all the way through the component tree: simply use this hook instead.
*/
export function useBlogPost(): BlogPostContextValue {
const blogPost = useContext(Context);
if (blogPost === null) {
throw new ReactContextError('BlogPostProvider');
}
return blogPost;
}

View file

@ -42,8 +42,7 @@ function useContextValue(content: PropDocContent): DocContextValue {
/** /**
* This is a very thin layer around the `content` received from the MDX loader. * This is a very thin layer around the `content` received from the MDX loader.
* It provides the component to be rendered and other metadata about the doc to * It provides metadata about the doc to the children tree.
* the children.
*/ */
export function DocProvider({ export function DocProvider({
children, children,

View file

@ -24,7 +24,13 @@ export {
} from './contexts/docSidebarItemsExpandedState'; } from './contexts/docSidebarItemsExpandedState';
export {DocsVersionProvider, useDocsVersion} from './contexts/docsVersion'; export {DocsVersionProvider, useDocsVersion} from './contexts/docsVersion';
export {DocsSidebarProvider, useDocsSidebar} from './contexts/docsSidebar'; export {DocsSidebarProvider, useDocsSidebar} from './contexts/docsSidebar';
export {DocProvider, useDoc, type DocContextValue} from './contexts/doc'; export {DocProvider, useDoc, type DocContextValue} from './contexts/doc';
export {
BlogPostProvider,
useBlogPost,
type BlogPostContextValue,
} from './contexts/blogPost';
export { export {
useDocsPreferredVersionByPluginId, useDocsPreferredVersionByPluginId,

View file

@ -6,15 +6,19 @@
*/ */
import React from 'react'; import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogPostAuthor'; import type {Props} from '@theme/BlogPostItem/Header/Author';
import styles from './styles.module.css'; import styles from './styles.module.css';
export default function ChangelogAuthor({author}: Props): JSX.Element { export default function ChangelogAuthor({
author,
className,
}: Props): JSX.Element {
const {name, url, imageURL} = author; const {name, url, imageURL} = author;
return ( return (
<div className="avatar margin-bottom--sm"> <div className={clsx('avatar margin-bottom--sm', className)}>
{imageURL && ( {imageURL && (
<Link className="avatar__photo-link avatar__photo" href={url}> <Link className="avatar__photo-link avatar__photo" href={url}>
<img <img

View file

@ -7,17 +7,21 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import ChangelogAuthor from '@theme/ChangelogAuthor'; import {useBlogPost} from '@docusaurus/theme-common/internal';
import BlogPostItemHeaderAuthor from '@theme/BlogPostItem/Header/Author';
import IconExpand from '@theme/IconExpand'; import IconExpand from '@theme/IconExpand';
import type {Props} from '@theme/BlogPostAuthors'; import type {Props} from '@theme/BlogPostItem/Header/Authors';
import styles from './styles.module.css'; import styles from './styles.module.css';
// Component responsible for the authors layout // Component responsible for the authors layout
export default function BlogPostAuthors({ export default function BlogPostAuthors({
authors, className,
assets,
}: Props): JSX.Element | null { }: Props): JSX.Element | null {
const {
metadata: {authors},
assets,
} = useBlogPost();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const authorsCount = authors.length; const authorsCount = authors.length;
if (authorsCount === 0) { if (authorsCount === 0) {
@ -29,10 +33,11 @@ export default function BlogPostAuthors({
className={clsx( className={clsx(
'margin-top--md margin-bottom--sm', 'margin-top--md margin-bottom--sm',
styles.imageOnlyAuthorRow, styles.imageOnlyAuthorRow,
className,
)}> )}>
{filteredAuthors.map((author, idx) => ( {filteredAuthors.map((author, idx) => (
<div className={styles.imageOnlyAuthorCol} key={idx}> <div className={styles.imageOnlyAuthorCol} key={idx}>
<ChangelogAuthor <BlogPostItemHeaderAuthor
author={{ author={{
...author, ...author,
// Handle author images using relative paths // Handle author images using relative paths

View file

@ -0,0 +1,35 @@
/**
* 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 {useBlogPost} from '@docusaurus/theme-common/internal';
import BlogPostItemHeaderTitle from '@theme/BlogPostItem/Header/Title';
import BlogPostItemHeaderInfo from '@theme/BlogPostItem/Header/Info';
import BlogPostItemHeaderAuthors from '@theme/BlogPostItem/Header/Authors';
import styles from './styles.module.css';
// Reduce changelog title size, but only on list view
function ChangelogTitle() {
const {isBlogPostPage} = useBlogPost();
return (
<BlogPostItemHeaderTitle
className={isBlogPostPage ? undefined : styles.changelogItemTitleList}
/>
);
}
export default function ChangelogItemHeader(): JSX.Element {
return (
<header>
<ChangelogTitle />
<BlogPostItemHeaderInfo />
<BlogPostItemHeaderAuthors />
</header>
);
}

View file

@ -0,0 +1,10 @@
/**
* 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.
*/
.changelogItemTitleList {
font-size: 2rem;
}

View file

@ -6,71 +6,18 @@
*/ */
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import ChangelogItemHeader from '@theme/ChangelogItem/Header';
import {MDXProvider} from '@mdx-js/react';
import Link from '@docusaurus/Link';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import {blogPostContainerID} from '@docusaurus/utils-common';
import MDXComponents from '@theme/MDXComponents';
import ChangelogAuthors from '@theme/ChangelogAuthors';
import type {Props} from '@theme/BlogPostItem'; import type {Props} from '@theme/BlogPostItem';
import BlogPostItemContainer from '@theme/BlogPostItem/Container';
import BlogPostItemContent from '@theme/BlogPostItem/Content';
import styles from './styles.module.css'; import styles from './styles.module.css';
export default function ChangelogItem(props: Props): JSX.Element { export default function ChangelogItem({children}: Props): JSX.Element {
const {withBaseUrl} = useBaseUrlUtils();
const {
children,
frontMatter,
assets,
metadata,
isBlogPostPage = false,
} = props;
const {date, formattedDate, permalink, title, authors} = metadata;
const image = assets.image ?? frontMatter.image;
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
return ( return (
<article <BlogPostItemContainer className={styles.changelogItemContainer}>
className={!isBlogPostPage ? 'margin-bottom--md' : undefined} <ChangelogItemHeader />
itemProp="blogPost" <BlogPostItemContent>{children}</BlogPostItemContent>
itemScope </BlogPostItemContainer>
itemType="http://schema.org/BlogPosting">
<header>
<TitleHeading
className={clsx(
isBlogPostPage ? styles.blogPostPageTitle : styles.blogPostTitle,
)}
itemProp="headline">
{isBlogPostPage ? (
title
) : (
<Link itemProp="url" to={permalink}>
{title}
</Link>
)}
</TitleHeading>
<div className={clsx(styles.blogPostData, 'margin-vert--md')}>
<time dateTime={date} itemProp="datePublished">
{formattedDate}
</time>
</div>
<ChangelogAuthors authors={authors} assets={assets} />
</header>
{image && (
<meta itemProp="image" content={withBaseUrl(image, {absolute: true})} />
)}
<div
// This ID is used for the feed generation to locate the main content
id={isBlogPostPage ? blogPostContainerID : undefined}
className="markdown"
itemProp="articleBody">
<MDXProvider components={MDXComponents}>{children}</MDXProvider>
</div>
</article>
); );
} }

View file

@ -5,18 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
.blogPostTitle { .changelogItemContainer {
font-size: 2rem; margin-bottom: 1rem;
}
.blogPostPageTitle {
font-size: 3rem;
}
.blogPostData {
font-size: 0.9rem;
}
.blogPostDetailsFull {
flex-direction: column;
} }

View file

@ -0,0 +1,81 @@
/**
* 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 Translate from '@docusaurus/Translate';
import Link from '@docusaurus/Link';
import styles from './styles.module.css';
function TwitterLink() {
return (
<Link href="https://twitter.com/docusaurus" className={styles.twitter}>
<b>Twitter</b>
<svg
style={{
fill: '#1da1f2',
position: 'relative',
left: 4,
top: 1,
marginRight: 8,
}}
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
</svg>
</Link>
);
}
function RssLink() {
return (
<Link href="pathname:///changelog/rss.xml" className={styles.rss}>
<b>
<Translate id="changelog.description.rssLink">RSS feeds</Translate>
</b>
<svg
style={{
fill: '#f26522',
position: 'relative',
left: 4,
top: 1,
marginRight: 8,
}}
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24">
<path d="M6.503 20.752c0 1.794-1.456 3.248-3.251 3.248-1.796 0-3.252-1.454-3.252-3.248 0-1.794 1.456-3.248 3.252-3.248 1.795.001 3.251 1.454 3.251 3.248zm-6.503-12.572v4.811c6.05.062 10.96 4.966 11.022 11.009h4.817c-.062-8.71-7.118-15.758-15.839-15.82zm0-3.368c10.58.046 19.152 8.594 19.183 19.188h4.817c-.03-13.231-10.755-23.954-24-24v4.812z" />
</svg>
</Link>
);
}
export default function ChangelogListHeader({
blogTitle,
}: {
blogTitle: string;
}): JSX.Element {
return (
<header className="margin-bottom--lg">
<h1 style={{fontSize: '3rem'}}>{blogTitle}</h1>
<p>
<Translate
id="changelog.description"
values={{
twitterLink: <TwitterLink />,
rssLink: <RssLink />,
}}>
{
'Subscribe through {rssLink} or follow us on {twitterLink} to stay up-to-date with new releases!'
}
</Translate>
</p>
</header>
);
}

View file

@ -5,18 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
.blogPostTitle {
font-size: 2rem;
}
.blogPostData {
font-size: 0.9rem;
}
.blogPostDetailsFull {
flex-direction: column;
}
.rss, .rss,
.rss:hover { .rss:hover {
color: #f26522; color: #f26522;

View file

@ -7,8 +7,6 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import Link from '@docusaurus/Link';
import { import {
PageMetadata, PageMetadata,
HtmlClassNameProvider, HtmlClassNameProvider,
@ -16,12 +14,12 @@ import {
} from '@docusaurus/theme-common'; } from '@docusaurus/theme-common';
import BlogLayout from '@theme/BlogLayout'; import BlogLayout from '@theme/BlogLayout';
import BlogListPaginator from '@theme/BlogListPaginator'; import BlogListPaginator from '@theme/BlogListPaginator';
import BlogPostItems from '@theme/BlogPostItems';
import SearchMetadata from '@theme/SearchMetadata'; import SearchMetadata from '@theme/SearchMetadata';
import ChangelogItem from '@theme/ChangelogItem'; import ChangelogItem from '@theme/ChangelogItem';
import ChangelogListHeader from '@theme/ChangelogList/Header';
import type {Props} from '@theme/BlogListPage'; import type {Props} from '@theme/BlogListPage';
import styles from './styles.module.css';
function ChangelogListMetadata(props: Props): JSX.Element { function ChangelogListMetadata(props: Props): JSX.Element {
const {metadata} = props; const {metadata} = props;
const {blogTitle, blogDescription} = metadata; const {blogTitle, blogDescription} = metadata;
@ -36,78 +34,10 @@ function ChangelogListMetadata(props: Props): JSX.Element {
function ChangelogListContent(props: Props): JSX.Element { function ChangelogListContent(props: Props): JSX.Element {
const {metadata, items, sidebar} = props; const {metadata, items, sidebar} = props;
const {blogTitle} = metadata; const {blogTitle} = metadata;
return ( return (
<BlogLayout sidebar={sidebar}> <BlogLayout sidebar={sidebar}>
<header className="margin-bottom--lg"> <ChangelogListHeader blogTitle={blogTitle} />
<h1 style={{fontSize: '3rem'}}>{blogTitle}</h1> <BlogPostItems items={items} component={ChangelogItem} />
<p>
<Translate
id="changelog.description"
values={{
twitterLink: (
<Link
href="https://twitter.com/docusaurus"
className={styles.twitter}>
<b>Twitter</b>
<svg
style={{
fill: '#1da1f2',
position: 'relative',
left: 4,
top: 1,
marginRight: 8,
}}
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
</svg>
</Link>
),
rssLink: (
<Link
href="pathname:///changelog/rss.xml"
className={styles.rss}>
<b>
<Translate id="changelog.description.rssLink">
RSS feeds
</Translate>
</b>
<svg
style={{
fill: '#f26522',
position: 'relative',
left: 4,
top: 1,
marginRight: 8,
}}
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24">
<path d="M6.503 20.752c0 1.794-1.456 3.248-3.251 3.248-1.796 0-3.252-1.454-3.252-3.248 0-1.794 1.456-3.248 3.252-3.248 1.795.001 3.251 1.454 3.251 3.248zm-6.503-12.572v4.811c6.05.062 10.96 4.966 11.022 11.009h4.817c-.062-8.71-7.118-15.758-15.839-15.82zm0-3.368c10.58.046 19.152 8.594 19.183 19.188h4.817c-.03-13.231-10.755-23.954-24-24v4.812z" />
</svg>
</Link>
),
}}>
{
'Subscribe through {rssLink} or follow us on {twitterLink} to stay up-to-date with new releases!'
}
</Translate>
</p>
</header>
{items.map(({content: BlogPostContent}) => (
<ChangelogItem
key={BlogPostContent.metadata.permalink}
frontMatter={BlogPostContent.frontMatter}
assets={BlogPostContent.assets}
metadata={BlogPostContent.metadata}
truncated={BlogPostContent.metadata.truncated}>
<BlogPostContent />
</ChangelogItem>
))}
<BlogListPaginator metadata={metadata} /> <BlogListPaginator metadata={metadata} />
</BlogLayout> </BlogLayout>
); );

View file

@ -5,103 +5,65 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React from 'react'; import React, {type ReactNode} from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import Translate from '@docusaurus/Translate'; import Translate from '@docusaurus/Translate';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import { import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common';
PageMetadata, import {BlogPostProvider, useBlogPost} from '@docusaurus/theme-common/internal';
HtmlClassNameProvider, import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
ThemeClassNames,
} from '@docusaurus/theme-common';
import BlogLayout from '@theme/BlogLayout'; import BlogLayout from '@theme/BlogLayout';
import ChangelogItem from '@theme/ChangelogItem'; import ChangelogItem from '@theme/ChangelogItem';
import ChangelogPaginator from '@theme/ChangelogPaginator'; import ChangelogPaginator from '@theme/ChangelogPaginator';
import TOC from '@theme/TOC'; import TOC from '@theme/TOC';
import type {Props} from '@theme/BlogPostPage'; import type {Props} from '@theme/BlogPostPage';
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
function ChangelogPageMetadata(props: Props): JSX.Element { function BackToIndexLink() {
const {content: BlogPostContents} = props; const {metadata} = useBlogPost();
const {assets, metadata} = BlogPostContents; // @ts-expect-error: we injected this
const {title, description, date, tags, authors, frontMatter} = metadata; const {listPageLink} = metadata;
const {keywords} = frontMatter;
const image = assets.image ?? frontMatter.image;
return ( return (
<PageMetadata <Link to={listPageLink}>
title={title} <Translate id="changelog.backLink"> Back to index page</Translate>
description={description} </Link>
keywords={keywords}
image={image}>
<meta property="og:type" content="article" />
<meta property="article:published_time" content={date} />
{authors.some((author) => author.url) && (
<meta
property="article:author"
content={authors
.map((author) => author.url)
.filter(Boolean)
.join(',')}
/>
)}
{tags.length > 0 && (
<meta
property="article:tag"
content={tags.map((tag) => tag.label).join(',')}
/>
)}
</PageMetadata>
); );
} }
function ChangelogPageContent(props: Props): JSX.Element { function ChangelogPageContent({
const {content: BlogPostContents, sidebar} = props; sidebar,
const {assets, metadata} = BlogPostContents; children,
const { }: {
nextItem, sidebar: BlogSidebar;
prevItem, children: ReactNode;
frontMatter, }): JSX.Element {
// @ts-expect-error: we injected this const {metadata, toc} = useBlogPost();
listPageLink, const {nextItem, prevItem, frontMatter} = metadata;
} = metadata;
const { const {
hide_table_of_contents: hideTableOfContents, hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel, toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel, toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter; } = frontMatter;
return ( return (
<> <BlogLayout
<PageMetadata /> sidebar={sidebar}
<BlogLayout toc={
sidebar={sidebar} !hideTableOfContents && toc.length > 0 ? (
toc={ <TOC
!hideTableOfContents && BlogPostContents.toc.length > 0 ? ( toc={toc}
<TOC minHeadingLevel={tocMinHeadingLevel}
toc={BlogPostContents.toc} maxHeadingLevel={tocMaxHeadingLevel}
minHeadingLevel={tocMinHeadingLevel} />
maxHeadingLevel={tocMaxHeadingLevel} ) : undefined
/> }>
) : undefined <BackToIndexLink />
}>
<Link to={listPageLink}>
<Translate id="changelog.backLink"> Back to index page</Translate>
</Link>
<ChangelogItem <ChangelogItem>{children}</ChangelogItem>
frontMatter={frontMatter}
assets={assets}
metadata={metadata}
isBlogPostPage>
<BlogPostContents />
</ChangelogItem>
{(nextItem || prevItem) && ( {(nextItem || prevItem) && (
<ChangelogPaginator nextItem={nextItem} prevItem={prevItem} /> <ChangelogPaginator nextItem={nextItem} prevItem={prevItem} />
)} )}
</BlogLayout> </BlogLayout>
</>
); );
} }
@ -109,14 +71,19 @@ function ChangelogPageContent(props: Props): JSX.Element {
// own ChangelogItem. We don't want to apply the swizzled item to the actual // own ChangelogItem. We don't want to apply the swizzled item to the actual
// blog. // blog.
export default function ChangelogPage(props: Props): JSX.Element { export default function ChangelogPage(props: Props): JSX.Element {
const ChangelogContent = props.content;
return ( return (
<HtmlClassNameProvider <BlogPostProvider content={props.content} isBlogPostPage>
className={clsx( <HtmlClassNameProvider
ThemeClassNames.wrapper.blogPages, className={clsx(
ThemeClassNames.page.blogPostPage, ThemeClassNames.wrapper.blogPages,
)}> ThemeClassNames.page.blogPostPage,
<ChangelogPageMetadata {...props} /> )}>
<ChangelogPageContent {...props} /> <BlogPostPageMetadata />
</HtmlClassNameProvider> <ChangelogPageContent sidebar={props.sidebar}>
<ChangelogContent />
</ChangelogPageContent>
</HtmlClassNameProvider>
</BlogPostProvider>
); );
} }

View file

@ -5,10 +5,16 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
declare module '@theme/ChangelogItem';
declare module '@theme/ChangelogAuthors';
declare module '@theme/ChangelogAuthor';
declare module '@theme/ChangelogPaginator'; declare module '@theme/ChangelogPaginator';
declare module '@theme/ChangelogItem';
declare module '@theme/ChangelogItem/Header';
declare module '@theme/ChangelogItem/Header/Author';
declare module '@theme/ChangelogItem/Header/Authors';
declare module '@theme/ChangelogList';
declare module '@theme/ChangelogList/Header';
declare module '@theme/IconExpand' { declare module '@theme/IconExpand' {
import type {ComponentProps} from 'react'; import type {ComponentProps} from 'react';