mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-04 01:09:20 +02:00
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:
parent
50f9fce29b
commit
f356e29938
56 changed files with 1670 additions and 706 deletions
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue