diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts index 3ec0e9d9f90a..8baaf6fe6300 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/blogUtils.test.ts @@ -15,9 +15,32 @@ import { getSourceToPermalink, paginateBlogPosts, type LinkifyParams, + sortBlogPosts, } from '../blogUtils'; import type {BlogBrokenMarkdownLink, BlogContentPaths} from '../types'; -import type {BlogPost} from '@docusaurus/plugin-content-blog'; +import type {BlogPost, BlogPostMetadata} from '@docusaurus/plugin-content-blog'; + +const defaultValuesForOtherKeys: Omit = { + source: '', + title: '', + formattedDate: '', + permalink: '', + description: '', + hasTruncateMarker: false, + authors: [], + frontMatter: {}, + tags: [], + unlisted: false, +}; +const createBlogPost = (args: Partial): BlogPost => ({ + id: '', + metadata: { + date: new Date(), + ...defaultValuesForOtherKeys, + ...args.metadata, + }, + content: args.content || '', +}); describe('truncate', () => { it('truncates texts', () => { @@ -214,6 +237,7 @@ describe('linkify', () => { frontMatter: {}, authors: [], formattedDate: '', + unlisted: false, }, content: '', }, @@ -272,3 +296,70 @@ describe('linkify', () => { } as BlogBrokenMarkdownLink); }); }); + +describe('blog sort', () => { + const blogPost2022 = createBlogPost({ + metadata: {date: new Date('2022-12-14'), ...defaultValuesForOtherKeys}, + }); + + const blogPost2023 = createBlogPost({ + metadata: {date: new Date('2023-12-14'), ...defaultValuesForOtherKeys}, + }); + const blogPost2024 = createBlogPost({ + metadata: {date: new Date('2024-12-14'), ...defaultValuesForOtherKeys}, + }); + + it('sort blog posts by descending date no return', () => { + const sortedBlogPosts = sortBlogPosts({ + blogPosts: [blogPost2023, blogPost2022, blogPost2024], + sortPosts: 'descending', + }); + expect(sortedBlogPosts).toEqual([blogPost2024, blogPost2023, blogPost2022]); + }); + + it('sort blog posts by ascending date no return', () => { + const sortedBlogPosts = sortBlogPosts({ + blogPosts: [blogPost2023, blogPost2022, blogPost2024], + sortPosts: 'ascending', + }); + expect(sortedBlogPosts).toEqual([blogPost2022, blogPost2023, blogPost2024]); + }); + + // it('sort blog posts by descending date with function return', () => { + // const sortedBlogPosts = sortBlogPosts({ + // blogPosts: [blogPost2023, blogPost2022, blogPost2024], + // sortPosts: ({blogPosts}) => + // [...blogPosts].sort( + // (a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(), + // ), + // }); + // expect(sortedBlogPosts).toEqual([blogPost2024, blogPost2023, blogPost2022]); + // }); + + // it('sort blog posts by ascending date with function return', () => { + // const sortedBlogPosts = sortBlogPosts({ + // blogPosts: [blogPost2023, blogPost2022, blogPost2024], + // sortPosts: ({blogPosts}) => + // [...blogPosts].sort( + // (b, a) => b.metadata.date.getTime() - a.metadata.date.getTime(), + // ), + // }); + // expect(sortedBlogPosts).toEqual([blogPost2022, blogPost2023, blogPost2024]); + // }); + + // it('sort blog posts with empty function', () => { + // const sortedBlogPosts = sortBlogPosts({ + // blogPosts: [blogPost2023, blogPost2022, blogPost2024], + // sortPosts: () => {}, + // }); + // expect(sortedBlogPosts).toEqual([blogPost2023, blogPost2022, blogPost2024]); + // }); + + // it('sort blog posts with function return empty array', () => { + // const sortedBlogPosts = sortBlogPosts({ + // blogPosts: [blogPost2023, blogPost2022, blogPost2024], + // sortPosts: () => [], + // }); + // expect(sortedBlogPosts).toEqual([blogPost2023, blogPost2022, blogPost2024]); + // }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts index 022d4ce749a9..1aff509d8853 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -88,6 +88,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { } as LoadContext, { path: 'invalid-blog-path', + sortPosts: 'descending', routeBasePath: 'blog', tagsBasePath: 'tags', authorsMapPath: 'authors.yml', @@ -128,6 +129,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { } as LoadContext, { path: 'blog', + sortPosts: 'descending', routeBasePath: 'blog', tagsBasePath: 'tags', authorsMapPath: 'authors.yml', @@ -171,6 +173,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { } as LoadContext, { path: 'blog', + sortPosts: 'descending', routeBasePath: 'blog', tagsBasePath: 'tags', authorsMapPath: 'authors.yml', @@ -224,6 +227,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { } as LoadContext, { path: 'blog', + sortPosts: 'descending', routeBasePath: 'blog', tagsBasePath: 'tags', authorsMapPath: 'authors.yml', diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 3bbb5301bf92..c190d8d8f181 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -36,6 +36,9 @@ import type { BlogPost, BlogTags, BlogPaginated, + Options, + SortPresets, + SortBlogPostsPreset, } from '@docusaurus/plugin-content-blog'; import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types'; @@ -363,6 +366,46 @@ async function processBlogSourceFile( }; } +const sortPresets: SortPresets = { + descending: ({blogPosts}) => + blogPosts.sort( + (a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(), + ), + ascending: ({blogPosts}) => + blogPosts.sort( + (a, b) => a.metadata.date.getTime() - b.metadata.date.getTime(), + ), +}; + +interface SortBlogPostsOptions { + blogPosts: BlogPost[]; + sortPosts: SortBlogPostsPreset; +} + +function getSortFunction(sortPosts: Options['sortPosts']) { + if (sortPosts === undefined) { + return sortPresets.descending; + } + const presetFn = sortPresets[sortPosts]; + if (!presetFn) { + throw new Error( + `sortPosts preset ${sortPosts} does not exist, valid presets are: ${Object.keys( + sortPresets, + ).join(', ')}`, + ); + } + return presetFn; +} + +export function sortBlogPosts({ + blogPosts, + sortPosts, +}: SortBlogPostsOptions): BlogPost[] { + getSortFunction(sortPosts); + + return blogPosts; +} + export async function generateBlogPosts( contentPaths: BlogContentPaths, context: LoadContext, @@ -405,14 +448,12 @@ export async function generateBlogPosts( await Promise.all(blogSourceFiles.map(doProcessBlogSourceFile)) ).filter(Boolean) as BlogPost[]; - blogPosts.sort( - (a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(), - ); + const sortedPosts = sortBlogPosts({ + blogPosts, + sortPosts: options.sortPosts, + }); - if (options.sortPosts === 'ascending') { - return blogPosts.reverse(); - } - return blogPosts; + return sortedPosts; } export type LinkifyParams = { diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 4605877ed75b..84da8ca284ef 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -107,11 +107,20 @@ export default async function pluginContentBlog( blogDescription, blogTitle, blogSidebarTitle, + processPosts, } = options; const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]); const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]); - const blogPosts = await generateBlogPosts(contentPaths, context, options); + let blogPosts = await generateBlogPosts(contentPaths, context, options); + const processedBlogPosts = processPosts({ + blogPosts, + }); + + if (processedBlogPosts !== undefined) { + blogPosts = processedBlogPosts; + } + const listedBlogPosts = blogPosts.filter(shouldBeListed); if (!blogPosts.length) { diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index 5b9889399a10..077cfdec54c4 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -50,6 +50,7 @@ export const DEFAULT_OPTIONS: PluginOptions = { authorsMapPath: 'authors.yml', readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), sortPosts: 'descending', + processPosts: () => undefined, }; const PluginOptionSchema = Joi.object({ @@ -129,9 +130,12 @@ const PluginOptionSchema = Joi.object({ }).default(DEFAULT_OPTIONS.feedOptions), authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath), readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime), - sortPosts: Joi.string() - .valid('descending', 'ascending') + sortPosts: Joi.alternatives() + .try(Joi.string().valid('descending', 'ascending')) .default(DEFAULT_OPTIONS.sortPosts), + processPosts: Joi.function() + .optional() + .default(() => DEFAULT_OPTIONS.processPosts), }).default(DEFAULT_OPTIONS); export function validateOptions({ 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 b1915f75196f..0eb3d12bf9da 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 @@ -333,6 +333,16 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the defaultReadingTime: ReadingTimeFunction; }, ) => number | undefined; + + export type ProcessBlogPostsFn = (params: { + blogPosts: BlogPost[]; + }) => void | BlogPost[]; + + export type SortBlogPostsPreset = 'ascending' | 'descending'; + + // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style + export type SortPresets = Record; + /** * Plugin options after normalization. */ @@ -418,7 +428,9 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the /** A callback to customize the reading time number displayed. */ readingTime: ReadingTimeFunctionOption; /** Governs the direction of blog post sorting. */ - sortPosts: 'ascending' | 'descending'; + sortPosts: SortBlogPostsPreset; + /** TODO process blog posts. */ + processPosts: ProcessBlogPostsFn; }; /** diff --git a/website/docs/api/plugins/plugin-content-blog.mdx b/website/docs/api/plugins/plugin-content-blog.mdx index 41a90b4058ab..bf5160e36015 100644 --- a/website/docs/api/plugins/plugin-content-blog.mdx +++ b/website/docs/api/plugins/plugin-content-blog.mdx @@ -73,7 +73,8 @@ Accepted fields: | `feedOptions.description` | `string` | \`$\{siteConfig.title} Blog\` | Description of the feed. | | `feedOptions.copyright` | `string` | `undefined` | Copyright message. | | `feedOptions.language` | `string` (See [documentation](http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes) for possible values) | `undefined` | Language metadata of the feed. | -| `sortPosts` | 'descending' \| 'ascending' | `'descending'` | Governs the direction of blog post sorting. | +| `sortPosts` | 'descending' \| 'ascending' \| | `'descending'` | Governs the direction of blog post sorting. | +| `processPosts` | ProcessBlogPostsFn | `undefined` | Function which process blog posts (filter, modify, delete, etc...). | ```mdx-code-block @@ -130,6 +131,14 @@ type CreateFeedItemsFn = (params: { }) => Promise; ``` +#### `ProcessBlogPostsFn` {#ProcessBlogPostsFn} + +```ts +type ProcessBlogPostsFn = (params: { + blogPosts: BlogPost[]; +}) => void | BlogPost[]; +``` + ### Example configuration {#ex-config} You can configure this plugin through preset options or plugin options.