feat(blog): authors page (#10216)

Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
Co-authored-by: slorber <slorber@users.noreply.github.com>
This commit is contained in:
ozaki 2024-08-01 17:30:49 +02:00 committed by GitHub
parent 50f9fce29b
commit f356e29938
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 1670 additions and 706 deletions

View file

@ -127,6 +127,27 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'The object mapping admonition type to a React component.\nUse it to add custom admonition type components, or replace existing ones.\nCan be ejected or wrapped (only manually, see our documentation).',
},
Blog: {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
eject: 'forbidden',
wrap: 'forbidden',
},
},
'Blog/Components': {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
eject: 'forbidden',
wrap: 'forbidden',
},
},
'Blog/Pages': {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
eject: 'forbidden',
wrap: 'forbidden',
},
},
CodeBlock: {
actions: {
eject: 'safe',

View file

@ -185,6 +185,30 @@ declare module '@theme/BackToTopButton' {
export default function BackToTopButton(): JSX.Element;
}
declare module '@theme/Blog/Components/Author' {
import type {Author} from '@docusaurus/plugin-content-blog';
export interface Props {
readonly as?: 'h1' | 'h2';
readonly author: Author;
readonly className?: string;
readonly count?: number;
}
export default function BlogAuthor(props: Props): JSX.Element;
}
declare module '@theme/Blog/Components/Author/Socials' {
import type {Author} from '@docusaurus/plugin-content-blog';
export interface Props {
readonly author: Author;
readonly className?: string;
}
export default function BlogAuthorSocials(props: Props): JSX.Element;
}
declare module '@theme/BlogListPaginator' {
import type {BlogPaginatedMetadata} from '@docusaurus/plugin-content-blog';
@ -291,31 +315,6 @@ declare module '@theme/BlogPostItem/Header/Info' {
export default function BlogPostItemHeaderInfo(): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Author' {
import type {Author} from '@docusaurus/plugin-content-blog';
export interface Props {
readonly author: Author;
readonly singleAuthor: boolean;
readonly className?: string;
}
export default function BlogPostItemHeaderAuthor(props: Props): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Author/Socials' {
import type {Author} from '@docusaurus/plugin-content-blog';
export interface Props {
readonly author: Author;
readonly className?: string;
}
export default function BlogPostItemHeaderAuthorSocials(
props: Props,
): JSX.Element;
}
declare module '@theme/BlogPostItem/Header/Authors' {
export interface Props {
readonly className?: string;

View file

@ -9,7 +9,7 @@ import type {ComponentType} from 'react';
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogPostItem/Header/Author/Socials';
import type {Props} from '@theme/Blog/Components/Author/Socials';
import Twitter from '@theme/Icon/Socials/Twitter';
import GitHub from '@theme/Icon/Socials/GitHub';
@ -50,10 +50,15 @@ function SocialLink({platform, link}: {platform: string; link: string}) {
);
}
export default function AuthorSocials({author}: {author: Props['author']}) {
export default function BlogAuthorSocials({
author,
}: {
author: Props['author'];
}): JSX.Element {
const entries = Object.entries(author.socials ?? {});
return (
<div className={styles.authorSocials}>
{Object.entries(author.socials ?? {}).map(([platform, linkUrl]) => {
{entries.map(([platform, linkUrl]) => {
return <SocialLink key={platform} platform={platform} link={linkUrl} />;
})}
</div>

View file

@ -10,7 +10,12 @@
}
.authorSocials {
margin-top: 0.2rem;
/*
This ensures that container takes height even if there's no social link
This keeps author names aligned even if only some have socials
*/
height: var(--docusaurus-blog-social-icon-size);
display: flex;
flex-wrap: wrap;
align-items: center;
@ -25,7 +30,7 @@
height: var(--docusaurus-blog-social-icon-size);
width: var(--docusaurus-blog-social-icon-size);
line-height: 0;
margin-right: 0.3rem;
margin-right: 0.4rem;
}
.authorSocialIcon {

View file

@ -0,0 +1,99 @@
/**
* 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, {type Props as LinkProps} from '@docusaurus/Link';
import AuthorSocials from '@theme/Blog/Components/Author/Socials';
import type {Props} from '@theme/Blog/Components/Author';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
function MaybeLink(props: LinkProps): JSX.Element {
if (props.href) {
return <Link {...props} />;
}
return <>{props.children}</>;
}
function AuthorTitle({title}: {title: string}) {
return (
<small className={styles.authorTitle} title={title}>
{title}
</small>
);
}
function AuthorName({name, as}: {name: string; as: Props['as']}) {
if (!as) {
return <span className={styles.authorName}>{name}</span>;
} else {
return (
<Heading as={as} className={styles.authorName}>
{name}
</Heading>
);
}
}
function AuthorBlogPostCount({count}: {count: number}) {
return <span className={clsx(styles.authorBlogPostCount)}>{count}</span>;
}
// Note: in the future we might want to have multiple "BlogAuthor" components
// Creating different display modes with the "as" prop may not be the best idea
// Explainer: https://kyleshevlin.com/prefer-multiple-compositions/
// For now, we almost use the same design for all cases, so it's good enough
export default function BlogAuthor({
as,
author,
className,
count,
}: Props): JSX.Element {
const {name, title, url, imageURL, email, page} = author;
const link =
page?.permalink || url || (email && `mailto:${email}`) || undefined;
return (
<div
className={clsx(
'avatar margin-bottom--sm',
className,
styles[`author-as-${as}`],
)}>
{imageURL && (
<MaybeLink href={link} className="avatar__photo-link">
<img
className={clsx('avatar__photo', styles.authorImage)}
src={imageURL}
alt={name}
/>
</MaybeLink>
)}
{(name || title) && (
<div className={clsx('avatar__intro', styles.authorDetails)}>
<div className="avatar__name">
{name && (
<MaybeLink href={link}>
<AuthorName name={name} as={as} />
</MaybeLink>
)}
{count && <AuthorBlogPostCount count={count} />}
</div>
{!!title && <AuthorTitle title={title} />}
{/*
We always render AuthorSocials even if there's none
This keeps other things aligned with flexbox layout
*/}
<AuthorSocials author={author} />
</div>
)}
</div>
);
}

View file

@ -0,0 +1,74 @@
/**
* 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.
*/
.authorImage {
--ifm-avatar-photo-size: 3.6rem;
}
.author-as-h1 .authorImage {
--ifm-avatar-photo-size: 7rem;
}
.author-as-h2 .authorImage {
--ifm-avatar-photo-size: 5.4rem;
}
.authorDetails {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-around;
}
.authorName {
font-size: 1.1rem;
line-height: 1.1rem;
display: flex;
flex-direction: row;
}
.author-as-h1 .authorName {
font-size: 2.4rem;
line-height: 2.4rem;
display: inline;
}
.author-as-h2 .authorName {
font-size: 1.4rem;
line-height: 1.4rem;
display: inline;
}
.authorTitle {
font-size: 0.8rem;
line-height: 0.8rem;
display: -webkit-box;
overflow: hidden;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.author-as-h1 .authorTitle {
font-size: 1.2rem;
line-height: 1.2rem;
}
.author-as-h2 .authorTitle {
font-size: 1rem;
line-height: 1rem;
}
.authorBlogPostCount {
background: var(--ifm-color-secondary);
color: var(--ifm-color-black);
font-size: 0.8rem;
line-height: 1.2;
border-radius: var(--ifm-global-radius);
padding: 0.1rem 0.4rem;
margin-left: 0.3rem;
}

View file

@ -0,0 +1,62 @@
/**
* 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, {type ReactNode} from 'react';
import clsx from 'clsx';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import {translateBlogAuthorsListPageTitle} from '@docusaurus/theme-common/internal';
import BlogLayout from '@theme/BlogLayout';
import type {Props} from '@theme/Blog/Pages/BlogAuthorsListPage';
import SearchMetadata from '@theme/SearchMetadata';
import Heading from '@theme/Heading';
import Author from '@theme/Blog/Components/Author';
import type {AuthorItemProp} from '@docusaurus/plugin-content-blog';
import styles from './styles.module.css';
function AuthorListItem({author}: {author: AuthorItemProp}) {
return (
<li className={styles.authorListItem}>
<Author as="h2" author={author} count={author.count} />
</li>
);
}
function AuthorsList({authors}: {authors: Props['authors']}) {
return (
<section className={clsx('margin-vert--lg', styles.authorsListSection)}>
<ul>
{authors.map((author) => (
<AuthorListItem key={author.key} author={author} />
))}
</ul>
</section>
);
}
export default function BlogAuthorsListPage({
authors,
sidebar,
}: Props): ReactNode {
const title: string = translateBlogAuthorsListPageTitle();
return (
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogAuthorsListPage,
)}>
<PageMetadata title={title} />
<SearchMetadata tag="blog_authors_list" />
<BlogLayout sidebar={sidebar}>
<Heading as="h1">{title}</Heading>
<AuthorsList authors={authors} />
</BlogLayout>
</HtmlClassNameProvider>
);
}

View file

@ -0,0 +1,11 @@
/**
* 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.
*/
.authorListItem {
list-style-type: none;
margin-bottom: 2rem;
}

View file

@ -0,0 +1,73 @@
/**
* 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 {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import {
useBlogAuthorPageTitle,
BlogAuthorsListViewAllLabel,
} from '@docusaurus/theme-common/internal';
import Link from '@docusaurus/Link';
import {useBlogMetadata} from '@docusaurus/plugin-content-blog/client';
import BlogLayout from '@theme/BlogLayout';
import BlogListPaginator from '@theme/BlogListPaginator';
import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/Blog/Pages/BlogAuthorsPostsPage';
import BlogPostItems from '@theme/BlogPostItems';
import Author from '@theme/Blog/Components/Author';
function Metadata({author}: Props): JSX.Element {
const title = useBlogAuthorPageTitle(author);
return (
<>
<PageMetadata title={title} />
<SearchMetadata tag="blog_authors_posts" />
</>
);
}
function ViewAllAuthorsLink() {
const {authorsListPath} = useBlogMetadata();
return (
<Link href={authorsListPath}>
<BlogAuthorsListViewAllLabel />
</Link>
);
}
function Content({author, items, sidebar, listMetadata}: Props): JSX.Element {
return (
<BlogLayout sidebar={sidebar}>
<header className="margin-bottom--xl">
<Author as="h1" author={author} />
{author.description && <p>{author.description}</p>}
<ViewAllAuthorsLink />
</header>
<hr />
<BlogPostItems items={items} />
<BlogListPaginator metadata={listMetadata} />
</BlogLayout>
);
}
export default function BlogAuthorsPostsPage(props: Props): JSX.Element {
return (
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogAuthorsPostsPage,
)}>
<Metadata {...props} />
<Content {...props} />
</HtmlClassNameProvider>
);
}

View file

@ -1,62 +0,0 @@
/**
* 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, {type Props as LinkProps} from '@docusaurus/Link';
import AuthorSocials from '@theme/BlogPostItem/Header/Author/Socials';
import type {Props} from '@theme/BlogPostItem/Header/Author';
import styles from './styles.module.css';
function MaybeLink(props: LinkProps): JSX.Element {
if (props.href) {
return <Link {...props} />;
}
return <>{props.children}</>;
}
function AuthorTitle({title}: {title: string}) {
return (
<small className={styles.authorTitle} title={title}>
{title}
</small>
);
}
export default function BlogPostItemHeaderAuthor({
// singleAuthor, // may be useful in the future, or for swizzle users
author,
className,
}: Props): JSX.Element {
const {name, title, url, socials, imageURL, email} = author;
const link = url || (email && `mailto:${email}`) || undefined;
const hasSocials = socials && Object.keys(socials).length > 0;
return (
<div className={clsx('avatar margin-bottom--sm', className)}>
{imageURL && (
<MaybeLink href={link} className="avatar__photo-link">
<img className="avatar__photo" src={imageURL} alt={name} />
</MaybeLink>
)}
{(name || title) && (
<div className="avatar__intro">
<div className="avatar__name">
<MaybeLink href={link}>
<span className={styles.authorName}>{name}</span>
</MaybeLink>
</div>
{!!title && <AuthorTitle title={title} />}
{hasSocials && <AuthorSocials author={author} />}
</div>
)}
</div>
);
}

View file

@ -1,21 +0,0 @@
/**
* 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.
*/
.authorName {
font-size: 1.1rem;
}
.authorTitle {
margin-top: 0.06rem;
font-size: 0.8rem;
line-height: 0.8rem;
display: -webkit-box;
overflow: hidden;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}

View file

@ -8,7 +8,7 @@
import React from 'react';
import clsx from 'clsx';
import {useBlogPost} from '@docusaurus/plugin-content-blog/client';
import BlogPostItemHeaderAuthor from '@theme/BlogPostItem/Header/Author';
import BlogAuthor from '@theme/Blog/Components/Author';
import type {Props} from '@theme/BlogPostItem/Header/Authors';
import styles from './styles.module.css';
@ -40,8 +40,7 @@ export default function BlogPostItemHeaderAuthors({
imageOnly ? styles.imageOnlyAuthorCol : styles.authorCol,
)}
key={idx}>
<BlogPostItemHeaderAuthor
singleAuthor={singleAuthor}
<BlogAuthor
author={{
...author,
// Handle author images using relative paths

View file

@ -7,13 +7,13 @@
import React from 'react';
import clsx from 'clsx';
import Translate, {translate} from '@docusaurus/Translate';
import Translate from '@docusaurus/Translate';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
usePluralForm,
} from '@docusaurus/theme-common';
import {useBlogTagsPostsPageTitle} from '@docusaurus/theme-common/internal';
import Link from '@docusaurus/Link';
import BlogLayout from '@theme/BlogLayout';
import BlogListPaginator from '@theme/BlogListPaginator';
@ -23,36 +23,6 @@ import BlogPostItems from '@theme/BlogPostItems';
import Unlisted from '@theme/Unlisted';
import Heading from '@theme/Heading';
// Very simple pluralization: probably good enough for now
function useBlogPostsPlural() {
const {selectMessage} = usePluralForm();
return (count: number) =>
selectMessage(
count,
translate(
{
id: 'theme.blog.post.plurals',
description:
'Pluralized label for "{count} posts". 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 post|{count} posts',
},
{count},
),
);
}
function useBlogTagsPostsPageTitle(tag: Props['tag']): string {
const blogPostsPlural = useBlogPostsPlural();
return translate(
{
id: 'theme.blog.tagTitle',
description: 'The title of the page for a blog tag',
message: '{nPosts} tagged with "{tagName}"',
},
{nPosts: blogPostsPlural(tag.count), tagName: tag.label},
);
}
function BlogTagsPostsPageMetadata({tag}: Props): JSX.Element {
const title = useBlogTagsPostsPageTitle(tag);
return (