diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-simple-slug-with-tags.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-simple-slug-with-tags.md new file mode 100644 index 0000000000..8cfe5ab945 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-simple-slug-with-tags.md @@ -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 diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-with-tags.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-with-tags.md new file mode 100644 index 0000000000..888efec535 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-with-tags.md @@ -0,0 +1,9 @@ +--- +slug: /another/tags +title: Another With Tag +date: 2020-08-15 + +tags: [tag1, tag2] +--- + +with tag diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-with-tags2.md b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-with-tags2.md new file mode 100644 index 0000000000..b69e2e9e17 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website-blog-with-tags/blog/another-with-tags2.md @@ -0,0 +1,9 @@ +--- +slug: /another/tags2 +title: Another With Tag +date: 2020-08-15 + +tags: [tag1, tag2] +--- + +with tag diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000000..2c0eb4a630 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/index.test.ts.snap @@ -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", + }, +} +`; diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index c6f1a8ca38..5e5217a9a3 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -24,6 +24,7 @@ function findByTitle( ): BlogPost | undefined { return blogPosts.find((v) => v.metadata.title === title); } + function getByTitle(blogPosts: BlogPost[], title: string): BlogPost { const post = findByTitle(blogPosts, title); if (!post) { @@ -99,6 +100,16 @@ describe('loadBlog', () => { return blogPosts; }; + const getBlogTags = async ( + siteDir: string, + pluginOptions: Partial = {}, + i18n: I18n = DefaultI18N, + ) => { + const plugin = await getPlugin(siteDir, pluginOptions, i18n); + const {blogTags} = (await plugin.loadContent!())!; + return blogTags; + }; + test('getPathsToWatch', async () => { const siteDir = path.join(__dirname, '__fixtures__', 'website'); const plugin = await getPlugin(siteDir); @@ -454,4 +465,18 @@ describe('loadBlog', () => { 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(); + }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index e084edea3d..254fb12cd9 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -14,6 +14,7 @@ import type { BlogContentPaths, BlogMarkdownLoaderOptions, BlogTags, + BlogPaginated, } from './types'; import { 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( blogPosts, (blogPost) => blogPost.metadata.tags, ); - return mapValues(groups, (group) => ({ - name: group.tag.label, - items: group.items.map((item) => item.id), - permalink: group.tag.permalink, - })); + + return mapValues(groups, (group) => { + const {tag, items: tagBlogPosts} = group; + 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 = diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index dd4eb9f34d..d267f41942 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -23,6 +23,7 @@ import { import {translateContent, getTranslationFiles} from './translations'; import type { + BlogTag, BlogTags, BlogContent, BlogItemsToMetadata, @@ -31,6 +32,7 @@ import type { BlogContentPaths, BlogMarkdownLoaderOptions, MetaData, + TagModule, } from './types'; import {PluginOptionSchema} from './pluginOptionSchema'; import type { @@ -46,6 +48,7 @@ import { generateBlogPosts, getSourceToPermalink, getBlogTags, + paginateBlogPosts, } from './blogUtils'; import {createBlogFeedFiles} from './feed'; import type { @@ -134,6 +137,7 @@ export default async function pluginContentBlog( blogListPaginated: [], blogTags: {}, 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 blogListPaginated: BlogPaginated[] = []; + const blogListPaginated: BlogPaginated[] = paginateBlogPosts({ + blogPosts, + blogTitle, + blogDescription, + postsPerPageOption, + basePageUrl: baseBlogUrl, + }); - function blogPaginationPermalink(page: number) { - return page > 0 - ? normalizeUrl([baseBlogUrl, `page/${page + 1}`]) - : 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, - blogTitle, - }, - items: blogPosts - .slice(page * postsPerPage, (page + 1) * postsPerPage) - .map((item) => item.id), - }); - } - - const blogTags: BlogTags = getBlogTags(blogPosts); + const blogTags: BlogTags = getBlogTags({ + blogPosts, + postsPerPageOption, + blogDescription, + blogTitle, + }); const tagsPath = normalizeUrl([baseBlogUrl, tagsBasePath]); @@ -345,50 +326,61 @@ export default async function pluginContentBlog( return; } - const tagsModule: TagsModule = {}; - - await Promise.all( - Object.keys(blogTags).map(async (tag) => { - const {name, items, permalink} = blogTags[tag]; - - // Refactor all this, see docs implementation - tagsModule[tag] = { + const tagsModule: TagsModule = Object.fromEntries( + Object.entries(blogTags).map(([tagKey, tag]) => { + const tagModule: TagModule = { allTagsPath: blogTagsListPath, - slug: tag, - name, - count: items.length, - permalink, + slug: tagKey, + name: tag.name, + count: tag.items.length, + permalink: tag.permalink, }; - - const tagsMetadataPath = await createData( - `${docuHash(permalink)}.json`, - JSON.stringify(tagsModule[tag], null, 2), - ); - - addRoute({ - path: permalink, - component: blogTagsPostsComponent, - exact: true, - modules: { - sidebar: aliasedSource(sidebarProp), - items: items.map((postID) => { - const metadata = blogItemsToMetadata[postID]; - return { - content: { - __import: true, - path: metadata.source, - query: { - truncated: true, - }, - }, - }; - }), - metadata: aliasedSource(tagsMetadataPath), - }, - }); + return [tag.name, tagModule]; }), ); + async function createTagRoutes(tag: BlogTag): Promise { + await Promise.all( + tag.pages.map(async (blogPaginated) => { + const {metadata, items} = blogPaginated; + const tagsMetadataPath = await createData( + `${docuHash(metadata.permalink)}.json`, + JSON.stringify(tagsModule[tag.name], null, 2), + ); + + const listMetadataPath = await createData( + `${docuHash(metadata.permalink)}-list.json`, + JSON.stringify(metadata, null, 2), + ); + + addRoute({ + path: metadata.permalink, + component: blogTagsPostsComponent, + exact: true, + modules: { + sidebar: aliasedSource(sidebarProp), + items: items.map((postID) => { + const blogPostMetadata = blogItemsToMetadata[postID]; + return { + content: { + __import: true, + path: blogPostMetadata.source, + query: { + truncated: true, + }, + }, + }; + }), + metadata: aliasedSource(tagsMetadataPath), + listMetadata: aliasedSource(listMetadataPath), + }, + }); + }), + ); + } + + await Promise.all(Object.values(blogTags).map(createTagRoutes)); + // Only create /tags page if there are tags. if (Object.keys(blogTags).length > 0) { const tagsListPath = await createData( diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index cd90496a00..618c97fb3a 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -259,10 +259,12 @@ declare module '@theme/BlogTagsPostsPage' { import type {BlogSidebar} from '@theme/BlogSidebar'; import type {Tag} from '@theme/BlogTagsListPage'; import type {Content} from '@theme/BlogPostPage'; + import type {Metadata} from '@theme/BlogListPage'; export interface Props { readonly sidebar: BlogSidebar; readonly metadata: Tag; + readonly listMetadata: Metadata; readonly items: readonly {readonly content: Content}[]; } diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index ed909f2b3c..d0a1ccec22 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -26,13 +26,17 @@ export interface BlogContent { } 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 { name: string; - items: string[]; + items: string[]; // blog post permalinks permalink: string; + pages: BlogPaginated[]; } export interface BlogPost { @@ -55,7 +59,7 @@ export interface BlogPaginatedMetadata { export interface BlogPaginated { metadata: BlogPaginatedMetadata; - items: string[]; + items: string[]; // blog post permalinks } export interface MetaData { diff --git a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx index 2c8e46a491..6791d74f9d 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx @@ -13,6 +13,7 @@ import BlogPostItem from '@theme/BlogPostItem'; import type {Props} from '@theme/BlogTagsPostsPage'; import Translate, {translate} from '@docusaurus/Translate'; import {ThemeClassNames, usePluralForm} from '@docusaurus/theme-common'; +import BlogListPaginator from '@theme/BlogListPaginator'; // Very simple pluralization: probably good enough for now function useBlogPostsPlural() { @@ -33,7 +34,7 @@ function useBlogPostsPlural() { } 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 blogPostsPlural = useBlogPostsPlural(); const title = translate( @@ -77,6 +78,7 @@ export default function BlogTagsPostsPage(props: Props): JSX.Element { ))} + ); } diff --git a/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx b/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx index 2245ed3dd1..4264d1a8d6 100644 --- a/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx +++ b/website/_dogfooding/_blog tests/2021-08-21-blog-post-toc-tests.mdx @@ -4,6 +4,7 @@ authors: - slorber toc_min_heading_level: 2 toc_max_heading_level: 4 +tags: [paginated-tag] --- diff --git a/website/_dogfooding/_blog tests/2021-08-22-no-author.md b/website/_dogfooding/_blog tests/2021-08-22-no-author.md index 497129fed0..9a04406bf8 100644 --- a/website/_dogfooding/_blog tests/2021-08-22-no-author.md +++ b/website/_dogfooding/_blog tests/2021-08-22-no-author.md @@ -1,3 +1,7 @@ +--- +tags: [paginated-tag] +--- + # Hmmm! This is a blog post from an anonymous author! diff --git a/website/_dogfooding/_blog tests/2021-08-23-multiple-authors.md b/website/_dogfooding/_blog tests/2021-08-23-multiple-authors.md index 81f0e3ab05..0c7c397955 100644 --- a/website/_dogfooding/_blog tests/2021-08-23-multiple-authors.md +++ b/website/_dogfooding/_blog tests/2021-08-23-multiple-authors.md @@ -8,6 +8,7 @@ tags: [ blog, docusaurus, + paginated-tag, long, long-long, long-long-long, diff --git a/website/_dogfooding/_blog tests/2021-09-13-dup-title.md b/website/_dogfooding/_blog tests/2021-09-13-dup-title.md index 82d4aea512..0f92f6a4b9 100644 --- a/website/_dogfooding/_blog tests/2021-09-13-dup-title.md +++ b/website/_dogfooding/_blog tests/2021-09-13-dup-title.md @@ -1,3 +1,7 @@ +--- +tags: [paginated-tag] +--- + # 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. diff --git a/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx b/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx index 005d1b52e8..a171eefe3f 100644 --- a/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx +++ b/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx @@ -2,7 +2,15 @@ title: Blog post MDX Feed tests authors: - 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 --- diff --git a/website/_dogfooding/_blog tests/2021-10-08-blog-post-mdx-require-feed-tests.mdx b/website/_dogfooding/_blog tests/2021-10-08-blog-post-mdx-require-feed-tests.mdx index 29c0cd60fa..1485ef0817 100644 --- a/website/_dogfooding/_blog tests/2021-10-08-blog-post-mdx-require-feed-tests.mdx +++ b/website/_dogfooding/_blog tests/2021-10-08-blog-post-mdx-require-feed-tests.mdx @@ -2,7 +2,15 @@ title: Blog post MDX require Feed tests authors: - 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 diff --git a/website/_dogfooding/_blog tests/2021-11-13-dup-title.md b/website/_dogfooding/_blog tests/2021-11-13-dup-title.md index 69d9b04f66..e5bf266ef2 100644 --- a/website/_dogfooding/_blog tests/2021-11-13-dup-title.md +++ b/website/_dogfooding/_blog tests/2021-11-13-dup-title.md @@ -1,3 +1,7 @@ +--- +tags: [paginated-tag] +--- + # Post with duplicate title I hope I'm still here diff --git a/website/_dogfooding/_blog tests/2022-01-20-image-only-authors.md b/website/_dogfooding/_blog tests/2022-01-20-image-only-authors.md index 6969846124..762f9db4b4 100644 --- a/website/_dogfooding/_blog tests/2022-01-20-image-only-authors.md +++ b/website/_dogfooding/_blog tests/2022-01-20-image-only-authors.md @@ -40,6 +40,7 @@ authors: url: https://github.com/anshulrgoyal - image_url: https://github.com/italicize.png url: https://github.com/italicize +tags: [paginated-tag] --- # Image-only authors diff --git a/website/_dogfooding/_blog tests/demo/2020-08-03-second-blog-intro.md b/website/_dogfooding/_blog tests/demo/2020-08-03-second-blog-intro.md index 391c91f496..c21273c78a 100644 --- a/website/_dogfooding/_blog tests/demo/2020-08-03-second-blog-intro.md +++ b/website/_dogfooding/_blog tests/demo/2020-08-03-second-blog-intro.md @@ -1,7 +1,7 @@ --- title: Using twice the blog plugin authors: [slorber] -tags: [blog, docusaurus] +tags: [paginated-tag, blog, docusaurus] --- Did you know you can use multiple instances of the same plugin? diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index f4642c8dee..1aac872f9f 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -116,7 +116,8 @@ const config = { require.resolve('./src/plugins/changelog/index.js'), { 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', blogSidebarTitle: 'Changelog', routeBasePath: '/changelog', @@ -127,10 +128,11 @@ const config = { feedOptions: { type: 'all', 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.`, language: 'en', - } + }, }, ], [