feat(content-blog): Allow pagination for BlogTagsPostsPage (#6221)

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
Muhammad Redho Ayassa 2022-02-04 00:33:13 +07:00 committed by GitHub
parent 01c6f15b15
commit 48f080ebca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 321 additions and 91 deletions

View file

@ -0,0 +1,13 @@
---
slug: /simple/slug/another
title: Another Simple Slug
date: 2020-08-15
author: Sébastien Lorber
author_title: Docusaurus maintainer
author_url: https://sebastienlorber.com
tags: [tag1]
---
simple url slug

View file

@ -0,0 +1,9 @@
---
slug: /another/tags
title: Another With Tag
date: 2020-08-15
tags: [tag1, tag2]
---
with tag

View file

@ -0,0 +1,9 @@
---
slug: /another/tags2
title: Another With Tag
date: 2020-08-15
tags: [tag1, tag2]
---
with tag

View file

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loadBlog test blog tags 1`] = `
Object {
"/blog/tags/tag-1": Object {
"items": Array [
"/simple/slug/another",
"/another/tags",
"/another/tags2",
],
"name": "tag1",
"pages": Array [
Object {
"items": Array [
"/simple/slug/another",
"/another/tags",
],
"metadata": Object {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": "/blog/tags/tag-1/page/2",
"page": 1,
"permalink": "/blog/tags/tag-1",
"postsPerPage": 2,
"previousPage": null,
"totalCount": 3,
"totalPages": 2,
},
},
Object {
"items": Array [
"/another/tags2",
],
"metadata": Object {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": null,
"page": 2,
"permalink": "/blog/tags/tag-1/page/2",
"postsPerPage": 2,
"previousPage": "/blog/tags/tag-1",
"totalCount": 3,
"totalPages": 2,
},
},
],
"permalink": "/blog/tags/tag-1",
},
"/blog/tags/tag-2": Object {
"items": Array [
"/another/tags",
"/another/tags2",
],
"name": "tag2",
"pages": Array [
Object {
"items": Array [
"/another/tags",
"/another/tags2",
],
"metadata": Object {
"blogDescription": "Blog",
"blogTitle": "Blog",
"nextPage": null,
"page": 1,
"permalink": "/blog/tags/tag-2",
"postsPerPage": 2,
"previousPage": null,
"totalCount": 2,
"totalPages": 1,
},
},
],
"permalink": "/blog/tags/tag-2",
},
}
`;

View file

@ -24,6 +24,7 @@ function findByTitle(
): BlogPost | undefined { ): BlogPost | undefined {
return blogPosts.find((v) => v.metadata.title === title); return blogPosts.find((v) => v.metadata.title === title);
} }
function getByTitle(blogPosts: BlogPost[], title: string): BlogPost { function getByTitle(blogPosts: BlogPost[], title: string): BlogPost {
const post = findByTitle(blogPosts, title); const post = findByTitle(blogPosts, title);
if (!post) { if (!post) {
@ -99,6 +100,16 @@ describe('loadBlog', () => {
return blogPosts; return blogPosts;
}; };
const getBlogTags = async (
siteDir: string,
pluginOptions: Partial<PluginOptions> = {},
i18n: I18n = DefaultI18N,
) => {
const plugin = await getPlugin(siteDir, pluginOptions, i18n);
const {blogTags} = (await plugin.loadContent!())!;
return blogTags;
};
test('getPathsToWatch', async () => { test('getPathsToWatch', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website'); const siteDir = path.join(__dirname, '__fixtures__', 'website');
const plugin = await getPlugin(siteDir); const plugin = await getPlugin(siteDir);
@ -454,4 +465,18 @@ describe('loadBlog', () => {
reversedOrder.map((x) => x.metadata.date), reversedOrder.map((x) => x.metadata.date),
); );
}); });
test('test blog tags', async () => {
const siteDir = path.join(
__dirname,
'__fixtures__',
'website-blog-with-tags',
);
const blogTags = await getBlogTags(siteDir, {
postsPerPage: 2,
});
expect(Object.keys(blogTags).length).toEqual(2);
expect(blogTags).toMatchSnapshot();
});
}); });

View file

@ -14,6 +14,7 @@ import type {
BlogContentPaths, BlogContentPaths,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
BlogTags, BlogTags,
BlogPaginated,
} from './types'; } from './types';
import { import {
parseMarkdownString, parseMarkdownString,
@ -50,16 +51,79 @@ export function getSourceToPermalink(
); );
} }
export function getBlogTags(blogPosts: BlogPost[]): BlogTags { export function paginateBlogPosts({
blogPosts,
basePageUrl,
blogTitle,
blogDescription,
postsPerPageOption,
}: {
blogPosts: BlogPost[];
basePageUrl: string;
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
}): BlogPaginated[] {
const totalCount = blogPosts.length;
const postsPerPage =
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
const numberOfPages = Math.ceil(totalCount / postsPerPage);
const pages: BlogPaginated[] = [];
function permalink(page: number) {
return page > 0 ? `${basePageUrl}/page/${page + 1}` : basePageUrl;
}
for (let page = 0; page < numberOfPages; page += 1) {
pages.push({
items: blogPosts
.slice(page * postsPerPage, (page + 1) * postsPerPage)
.map((item) => item.id),
metadata: {
permalink: permalink(page),
page: page + 1,
postsPerPage,
totalPages: numberOfPages,
totalCount,
previousPage: page !== 0 ? permalink(page - 1) : null,
nextPage: page < numberOfPages - 1 ? permalink(page + 1) : null,
blogDescription,
blogTitle,
},
});
}
return pages;
}
export function getBlogTags({
blogPosts,
...params
}: {
blogPosts: BlogPost[];
blogTitle: string;
blogDescription: string;
postsPerPageOption: number | 'ALL';
}): BlogTags {
const groups = groupTaggedItems( const groups = groupTaggedItems(
blogPosts, blogPosts,
(blogPost) => blogPost.metadata.tags, (blogPost) => blogPost.metadata.tags,
); );
return mapValues(groups, (group) => ({
name: group.tag.label, return mapValues(groups, (group) => {
items: group.items.map((item) => item.id), const {tag, items: tagBlogPosts} = group;
permalink: group.tag.permalink, return {
})); name: tag.label,
items: tagBlogPosts.map((item) => item.id),
permalink: tag.permalink,
pages: paginateBlogPosts({
blogPosts: tagBlogPosts,
basePageUrl: group.tag.permalink,
...params,
}),
};
});
} }
const DATE_FILENAME_REGEX = const DATE_FILENAME_REGEX =

View file

@ -23,6 +23,7 @@ import {
import {translateContent, getTranslationFiles} from './translations'; import {translateContent, getTranslationFiles} from './translations';
import type { import type {
BlogTag,
BlogTags, BlogTags,
BlogContent, BlogContent,
BlogItemsToMetadata, BlogItemsToMetadata,
@ -31,6 +32,7 @@ import type {
BlogContentPaths, BlogContentPaths,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
MetaData, MetaData,
TagModule,
} from './types'; } from './types';
import {PluginOptionSchema} from './pluginOptionSchema'; import {PluginOptionSchema} from './pluginOptionSchema';
import type { import type {
@ -46,6 +48,7 @@ import {
generateBlogPosts, generateBlogPosts,
getSourceToPermalink, getSourceToPermalink,
getBlogTags, getBlogTags,
paginateBlogPosts,
} from './blogUtils'; } from './blogUtils';
import {createBlogFeedFiles} from './feed'; import {createBlogFeedFiles} from './feed';
import type { import type {
@ -134,6 +137,7 @@ export default async function pluginContentBlog(
blogListPaginated: [], blogListPaginated: [],
blogTags: {}, blogTags: {},
blogTagsListPath: null, blogTagsListPath: null,
blogTagsPaginated: [],
}; };
} }
@ -157,45 +161,22 @@ export default async function pluginContentBlog(
} }
}); });
// Blog pagination routes.
// Example: `/blog`, `/blog/page/1`, `/blog/page/2`
const totalCount = blogPosts.length;
const postsPerPage =
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
const numberOfPages = Math.ceil(totalCount / postsPerPage);
const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]); const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
const blogListPaginated: BlogPaginated[] = []; const blogListPaginated: BlogPaginated[] = paginateBlogPosts({
blogPosts,
blogTitle,
blogDescription,
postsPerPageOption,
basePageUrl: baseBlogUrl,
});
function blogPaginationPermalink(page: number) { const blogTags: BlogTags = getBlogTags({
return page > 0 blogPosts,
? normalizeUrl([baseBlogUrl, `page/${page + 1}`]) postsPerPageOption,
: baseBlogUrl;
}
for (let page = 0; page < numberOfPages; page += 1) {
blogListPaginated.push({
metadata: {
permalink: blogPaginationPermalink(page),
page: page + 1,
postsPerPage,
totalPages: numberOfPages,
totalCount,
previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
nextPage:
page < numberOfPages - 1
? blogPaginationPermalink(page + 1)
: null,
blogDescription, blogDescription,
blogTitle, blogTitle,
},
items: blogPosts
.slice(page * postsPerPage, (page + 1) * postsPerPage)
.map((item) => item.id),
}); });
}
const blogTags: BlogTags = getBlogTags(blogPosts);
const tagsPath = normalizeUrl([baseBlogUrl, tagsBasePath]); const tagsPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
@ -345,38 +326,45 @@ export default async function pluginContentBlog(
return; return;
} }
const tagsModule: TagsModule = {}; const tagsModule: TagsModule = Object.fromEntries(
Object.entries(blogTags).map(([tagKey, tag]) => {
await Promise.all( const tagModule: TagModule = {
Object.keys(blogTags).map(async (tag) => {
const {name, items, permalink} = blogTags[tag];
// Refactor all this, see docs implementation
tagsModule[tag] = {
allTagsPath: blogTagsListPath, allTagsPath: blogTagsListPath,
slug: tag, slug: tagKey,
name, name: tag.name,
count: items.length, count: tag.items.length,
permalink, permalink: tag.permalink,
}; };
return [tag.name, tagModule];
}),
);
async function createTagRoutes(tag: BlogTag): Promise<void> {
await Promise.all(
tag.pages.map(async (blogPaginated) => {
const {metadata, items} = blogPaginated;
const tagsMetadataPath = await createData( const tagsMetadataPath = await createData(
`${docuHash(permalink)}.json`, `${docuHash(metadata.permalink)}.json`,
JSON.stringify(tagsModule[tag], null, 2), JSON.stringify(tagsModule[tag.name], null, 2),
);
const listMetadataPath = await createData(
`${docuHash(metadata.permalink)}-list.json`,
JSON.stringify(metadata, null, 2),
); );
addRoute({ addRoute({
path: permalink, path: metadata.permalink,
component: blogTagsPostsComponent, component: blogTagsPostsComponent,
exact: true, exact: true,
modules: { modules: {
sidebar: aliasedSource(sidebarProp), sidebar: aliasedSource(sidebarProp),
items: items.map((postID) => { items: items.map((postID) => {
const metadata = blogItemsToMetadata[postID]; const blogPostMetadata = blogItemsToMetadata[postID];
return { return {
content: { content: {
__import: true, __import: true,
path: metadata.source, path: blogPostMetadata.source,
query: { query: {
truncated: true, truncated: true,
}, },
@ -384,10 +372,14 @@ export default async function pluginContentBlog(
}; };
}), }),
metadata: aliasedSource(tagsMetadataPath), metadata: aliasedSource(tagsMetadataPath),
listMetadata: aliasedSource(listMetadataPath),
}, },
}); });
}), }),
); );
}
await Promise.all(Object.values(blogTags).map(createTagRoutes));
// Only create /tags page if there are tags. // Only create /tags page if there are tags.
if (Object.keys(blogTags).length > 0) { if (Object.keys(blogTags).length > 0) {

View file

@ -259,10 +259,12 @@ declare module '@theme/BlogTagsPostsPage' {
import type {BlogSidebar} from '@theme/BlogSidebar'; import type {BlogSidebar} from '@theme/BlogSidebar';
import type {Tag} from '@theme/BlogTagsListPage'; import type {Tag} from '@theme/BlogTagsListPage';
import type {Content} from '@theme/BlogPostPage'; import type {Content} from '@theme/BlogPostPage';
import type {Metadata} from '@theme/BlogListPage';
export interface Props { export interface Props {
readonly sidebar: BlogSidebar; readonly sidebar: BlogSidebar;
readonly metadata: Tag; readonly metadata: Tag;
readonly listMetadata: Metadata;
readonly items: readonly {readonly content: Content}[]; readonly items: readonly {readonly content: Content}[];
} }

View file

@ -26,13 +26,17 @@ export interface BlogContent {
} }
export interface BlogTags { export interface BlogTags {
[key: string]: BlogTag; // TODO, the key is the tag slug/permalink
// This is due to legacy frontmatter: tags: [{label: "xyz", permalink: "/1"}, {label: "xyz", permalink: "/2"}
// Soon we should forbid declaring permalink through frontmatter
[tagKey: string]: BlogTag;
} }
export interface BlogTag { export interface BlogTag {
name: string; name: string;
items: string[]; items: string[]; // blog post permalinks
permalink: string; permalink: string;
pages: BlogPaginated[];
} }
export interface BlogPost { export interface BlogPost {
@ -55,7 +59,7 @@ export interface BlogPaginatedMetadata {
export interface BlogPaginated { export interface BlogPaginated {
metadata: BlogPaginatedMetadata; metadata: BlogPaginatedMetadata;
items: string[]; items: string[]; // blog post permalinks
} }
export interface MetaData { export interface MetaData {

View file

@ -13,6 +13,7 @@ import BlogPostItem from '@theme/BlogPostItem';
import type {Props} from '@theme/BlogTagsPostsPage'; import type {Props} from '@theme/BlogTagsPostsPage';
import Translate, {translate} from '@docusaurus/Translate'; import Translate, {translate} from '@docusaurus/Translate';
import {ThemeClassNames, usePluralForm} from '@docusaurus/theme-common'; import {ThemeClassNames, usePluralForm} from '@docusaurus/theme-common';
import BlogListPaginator from '@theme/BlogListPaginator';
// Very simple pluralization: probably good enough for now // Very simple pluralization: probably good enough for now
function useBlogPostsPlural() { function useBlogPostsPlural() {
@ -33,7 +34,7 @@ function useBlogPostsPlural() {
} }
export default function BlogTagsPostsPage(props: Props): JSX.Element { export default function BlogTagsPostsPage(props: Props): JSX.Element {
const {metadata, items, sidebar} = props; const {metadata, items, sidebar, listMetadata} = props;
const {allTagsPath, name: tagName, count} = metadata; const {allTagsPath, name: tagName, count} = metadata;
const blogPostsPlural = useBlogPostsPlural(); const blogPostsPlural = useBlogPostsPlural();
const title = translate( const title = translate(
@ -77,6 +78,7 @@ export default function BlogTagsPostsPage(props: Props): JSX.Element {
<BlogPostContent /> <BlogPostContent />
</BlogPostItem> </BlogPostItem>
))} ))}
<BlogListPaginator metadata={listMetadata} />
</BlogLayout> </BlogLayout>
); );
} }

View file

@ -4,6 +4,7 @@ authors:
- slorber - slorber
toc_min_heading_level: 2 toc_min_heading_level: 2
toc_max_heading_level: 4 toc_max_heading_level: 4
tags: [paginated-tag]
--- ---
<!-- truncate --> <!-- truncate -->

View file

@ -1,3 +1,7 @@
---
tags: [paginated-tag]
---
# Hmmm! # Hmmm!
This is a blog post from an anonymous author! This is a blog post from an anonymous author!

View file

@ -8,6 +8,7 @@ tags:
[ [
blog, blog,
docusaurus, docusaurus,
paginated-tag,
long, long,
long-long, long-long,
long-long-long, long-long-long,

View file

@ -1,3 +1,7 @@
---
tags: [paginated-tag]
---
# Post with duplicate title # Post with duplicate title
See https://github.com/facebook/docusaurus/issues/6059. This one and [2021-11-13-dup-title.md](./2021-11-13-dup-title.md) should both show up. See https://github.com/facebook/docusaurus/issues/6059. This one and [2021-11-13-dup-title.md](./2021-11-13-dup-title.md) should both show up.

View file

@ -2,7 +2,15 @@
title: Blog post MDX Feed tests title: Blog post MDX Feed tests
authors: authors:
- slorber - slorber
tags: [blog, docusaurus, long-long, long-long-long, long-long-long-long] tags:
[
paginated-tag,
blog,
docusaurus,
long-long,
long-long-long,
long-long-long-long,
]
hide_reading_time: true hide_reading_time: true
--- ---

View file

@ -2,7 +2,15 @@
title: Blog post MDX require Feed tests title: Blog post MDX require Feed tests
authors: authors:
- slorber - slorber
tags: [blog, docusaurus, long-long, long-long-long, long-long-long-long] tags:
[
paginated-tag,
blog,
docusaurus,
long-long,
long-long-long,
long-long-long-long,
]
--- ---
Some MDX tests, mostly to test how the RSS feed render those Some MDX tests, mostly to test how the RSS feed render those

View file

@ -1,3 +1,7 @@
---
tags: [paginated-tag]
---
# Post with duplicate title # Post with duplicate title
I hope I'm still here I hope I'm still here

View file

@ -40,6 +40,7 @@ authors:
url: https://github.com/anshulrgoyal url: https://github.com/anshulrgoyal
- image_url: https://github.com/italicize.png - image_url: https://github.com/italicize.png
url: https://github.com/italicize url: https://github.com/italicize
tags: [paginated-tag]
--- ---
# Image-only authors # Image-only authors

View file

@ -1,7 +1,7 @@
--- ---
title: Using twice the blog plugin title: Using twice the blog plugin
authors: [slorber] authors: [slorber]
tags: [blog, docusaurus] tags: [paginated-tag, blog, docusaurus]
--- ---
Did you know you can use multiple instances of the same plugin? Did you know you can use multiple instances of the same plugin?

View file

@ -116,7 +116,8 @@ const config = {
require.resolve('./src/plugins/changelog/index.js'), require.resolve('./src/plugins/changelog/index.js'),
{ {
blogTitle: 'Docusaurus changelog', blogTitle: 'Docusaurus changelog',
blogDescription: 'Keep yourself up-to-date about new features in every release', blogDescription:
'Keep yourself up-to-date about new features in every release',
blogSidebarCount: 'ALL', blogSidebarCount: 'ALL',
blogSidebarTitle: 'Changelog', blogSidebarTitle: 'Changelog',
routeBasePath: '/changelog', routeBasePath: '/changelog',
@ -127,10 +128,11 @@ const config = {
feedOptions: { feedOptions: {
type: 'all', type: 'all',
title: 'Docusaurus changelog', title: 'Docusaurus changelog',
description: 'Keep yourself up-to-date about new features in every release', description:
'Keep yourself up-to-date about new features in every release',
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
language: 'en', language: 'en',
} },
}, },
], ],
[ [