From 022e00554ec18064966e964abf1d45060bc4ad24 Mon Sep 17 00:00:00 2001 From: John Reilly Date: Thu, 29 Dec 2022 12:31:32 +0000 Subject: [PATCH] feat(blog): add options.createFeedItems to filter/limit/transform feed items (#8378) Co-authored-by: sebastienlorber --- .../__tests__/__snapshots__/feed.test.ts.snap | 97 +++++++++++++++++++ .../src/__tests__/feed.test.ts | 52 ++++++++++ .../src/feed.ts | 37 +++++-- .../src/options.ts | 1 + .../src/plugin-content-blog.d.ts | 31 +++++- .../docs/api/plugins/plugin-content-blog.md | 20 ++++ website/docs/blog.mdx | 19 ++++ 7 files changed, 249 insertions(+), 8 deletions(-) diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap index b15b03340640..ae568922d131 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap @@ -1,5 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`atom filters to the first two entries 1`] = ` +[ + " + + https://docusaurus.io/myBaseUrl/blog + Hello Blog + 2021-03-06T00:00:00.000Z + https://github.com/jpmonette/feed + + Hello Blog + https://docusaurus.io/myBaseUrl/image/favicon.ico + Copyright + + <![CDATA[MDX Blog Sample with require calls]]> + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + + 2021-03-06T00:00:00.000Z + + Test MDX with require calls

]]>
+
+ + <![CDATA[Full Blog Sample]]> + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + + 2021-03-05T00:00:00.000Z + + HTML Heading 1

HTML Heading 2

HTML Paragraph

Import DOM

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
  • list1
  • list2
  • list3
  • list1
  • list2
  • list3

Normal Text Italics Text Bold Text

linkimage

]]>
+
+
", +] +`; + exports[`atom has feed item for each post 1`] = ` [ " @@ -84,6 +116,37 @@ exports[`atom has feed item for each post 1`] = ` ] `; +exports[`json filters to the first two entries 1`] = ` +[ + "{ + "version": "https://jsonfeed.org/version/1", + "title": "Hello Blog", + "home_page_url": "https://docusaurus.io/myBaseUrl/blog", + "description": "Hello Blog", + "items": [ + { + "id": "https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post", + "content_html": "

Test MDX with require calls

", + "url": "https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post", + "title": "MDX Blog Sample with require calls", + "summary": "Test MDX with require calls", + "date_modified": "2021-03-06T00:00:00.000Z", + "tags": [] + }, + { + "id": "https://docusaurus.io/myBaseUrl/blog/mdx-blog-post", + "content_html": "

HTML Heading 1

HTML Heading 2

HTML Paragraph

Import DOM

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5

Normal Text Italics Text Bold Text

link\\"image\\"

", + "url": "https://docusaurus.io/myBaseUrl/blog/mdx-blog-post", + "title": "Full Blog Sample", + "summary": "HTML Heading 1", + "date_modified": "2021-03-05T00:00:00.000Z", + "tags": [] + } + ] +}", +] +`; + exports[`json has feed item for each post 1`] = ` [ "{ @@ -171,6 +234,40 @@ exports[`json has feed item for each post 1`] = ` ] `; +exports[`rss filters to the first two entries 1`] = ` +[ + " + + + Hello Blog + https://docusaurus.io/myBaseUrl/blog + Hello Blog + Sat, 06 Mar 2021 00:00:00 GMT + https://validator.w3.org/feed/docs/rss2.html + https://github.com/jpmonette/feed + en + Copyright + + <![CDATA[MDX Blog Sample with require calls]]> + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + https://docusaurus.io/myBaseUrl/blog/mdx-require-blog-post + Sat, 06 Mar 2021 00:00:00 GMT + + Test MDX with require calls

]]>
+
+ + <![CDATA[Full Blog Sample]]> + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + https://docusaurus.io/myBaseUrl/blog/mdx-blog-post + Fri, 05 Mar 2021 00:00:00 GMT + + HTML Heading 1

HTML Heading 2

HTML Paragraph

Import DOM

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
  • list1
  • list2
  • list3
  • list1
  • list2
  • list3

Normal Text Italics Text Bold Text

linkimage

]]>
+
+
+
", +] +`; + exports[`rss has feed item for each post 1`] = ` [ " 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 83d0af77157c..6592c09d7aa5 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -143,4 +143,56 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { ).toMatchSnapshot(); fsMock.mockClear(); }); + + it('filters to the first two entries', async () => { + const siteDir = path.join(__dirname, '__fixtures__', 'website'); + const outDir = path.join(siteDir, 'build-snap'); + const siteConfig = { + title: 'Hello', + baseUrl: '/myBaseUrl/', + url: 'https://docusaurus.io', + favicon: 'image/favicon.ico', + }; + + // Build is quite difficult to mock, so we built the blog beforehand and + // copied the output to the fixture... + await testGenerateFeeds( + { + siteDir, + siteConfig, + i18n: DefaultI18N, + outDir, + } as LoadContext, + { + path: 'blog', + routeBasePath: 'blog', + tagsBasePath: 'tags', + authorsMapPath: 'authors.yml', + include: DEFAULT_OPTIONS.include, + exclude: DEFAULT_OPTIONS.exclude, + feedOptions: { + type: [feedType], + copyright: 'Copyright', + createFeedItems: async (params) => { + const {blogPosts, defaultCreateFeedItems, ...rest} = params; + const blogPostsFiltered = blogPosts.filter( + (item, index) => index < 2, + ); + return defaultCreateFeedItems({ + blogPosts: blogPostsFiltered, + ...rest, + }); + }, + }, + readingTime: ({content, defaultReadingTime}) => + defaultReadingTime({content}), + truncateMarker: //, + } as PluginOptions, + ); + + expect( + fsMock.mock.calls.map((call) => call[1] as string), + ).toMatchSnapshot(); + fsMock.mockClear(); + }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/feed.ts b/packages/docusaurus-plugin-content-blog/src/feed.ts index ea73e7a6a559..dc9a7b85f84c 100644 --- a/packages/docusaurus-plugin-content-blog/src/feed.ts +++ b/packages/docusaurus-plugin-content-blog/src/feed.ts @@ -8,7 +8,7 @@ import path from 'path'; import fs from 'fs-extra'; import logger from '@docusaurus/logger'; -import {Feed, type Author as FeedAuthor, type Item as FeedItem} from 'feed'; +import {Feed, type Author as FeedAuthor} from 'feed'; import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils'; import {blogPostContainerID} from '@docusaurus/utils-common'; import {load as cheerioLoad} from 'cheerio'; @@ -18,6 +18,7 @@ import type { PluginOptions, Author, BlogPost, + BlogFeedItem, } from '@docusaurus/plugin-content-blog'; async function generateBlogFeed({ @@ -54,11 +55,37 @@ async function generateBlogFeed({ copyright: feedOptions.copyright, }); + const createFeedItems = + options.feedOptions.createFeedItems ?? defaultCreateFeedItems; + + const feedItems = await createFeedItems({ + blogPosts, + siteConfig, + outDir, + defaultCreateFeedItems, + }); + + feedItems.forEach(feed.addItem); + + return feed; +} + +async function defaultCreateFeedItems({ + blogPosts, + siteConfig, + outDir, +}: { + blogPosts: BlogPost[]; + siteConfig: DocusaurusConfig; + outDir: string; +}): Promise { + const {url: siteUrl} = siteConfig; + function toFeedAuthor(author: Author): FeedAuthor { return {name: author.name, link: author.url, email: author.email}; } - await Promise.all( + return Promise.all( blogPosts.map(async (post) => { const { metadata: { @@ -79,7 +106,7 @@ async function generateBlogFeed({ const $ = cheerioLoad(content); const link = normalizeUrl([siteUrl, permalink]); - const feedItem: FeedItem = { + const feedItem: BlogFeedItem = { title: metadataTitle, id: link, link, @@ -99,9 +126,7 @@ async function generateBlogFeed({ return feedItem; }), - ).then((items) => items.forEach(feed.addItem)); - - return feed; + ); } async function createBlogFeedFile({ diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index 804169f73392..0ee9e50f8f0c 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -122,6 +122,7 @@ const PluginOptionSchema = Joi.object({ .default(DEFAULT_OPTIONS.feedOptions.copyright), }), language: Joi.string(), + createFeedItems: Joi.function(), }).default(DEFAULT_OPTIONS.feedOptions), authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath), readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime), 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 96c288d335f4..bcce2e7c70ec 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 @@ -9,12 +9,19 @@ declare module '@docusaurus/plugin-content-blog' { import type {LoadedMDXContent} from '@docusaurus/mdx-loader'; import type {MDXOptions} from '@docusaurus/mdx-loader'; import type {FrontMatterTag, Tag} from '@docusaurus/utils'; - import type {Plugin, LoadContext} from '@docusaurus/types'; + import type {DocusaurusConfig, Plugin, LoadContext} from '@docusaurus/types'; + import type {Item as FeedItem} from 'feed'; import type {Overwrite} from 'utility-types'; export type Assets = { /** - * If `metadata.image` is a collocated image path, this entry will be the + * If `metadata.yarn workspace website typecheck +4 +yarn workspace v1.22.19yarn workspace website typecheck +4 +yarn workspace v1.22.19yarn workspace website typecheck +4 +yarn workspace v1.22.19image` is a collocated image path, this entry will be the * bundler-generated image path. Otherwise, it's empty, and the image URL * should be accessed through `frontMatter.image`. */ @@ -263,6 +270,24 @@ declare module '@docusaurus/plugin-content-blog' { copyright: string; /** Language of the feed. */ language?: string; + /** Allow control over the construction of BlogFeedItems */ + createFeedItems?: CreateFeedItemsFn; + }; + + type DefaultCreateFeedItemsParams = { + blogPosts: BlogPost[]; + siteConfig: DocusaurusConfig; + outDir: string; + }; + + type CreateFeedItemsFn = ( + params: CreateFeedItemsParams, + ) => Promise; + + type CreateFeedItemsParams = DefaultCreateFeedItemsParams & { + defaultCreateFeedItems: ( + params: DefaultCreateFeedItemsParams, + ) => Promise; }; /** @@ -451,6 +476,8 @@ declare module '@docusaurus/plugin-content-blog' { content: string; }; + export type BlogFeedItem = FeedItem; + export type BlogPaginatedMetadata = { /** Title of the entire blog. */ readonly blogTitle: string; diff --git a/website/docs/api/plugins/plugin-content-blog.md b/website/docs/api/plugins/plugin-content-blog.md index d31d00c00b3b..4e1ab5fcd0e9 100644 --- a/website/docs/api/plugins/plugin-content-blog.md +++ b/website/docs/api/plugins/plugin-content-blog.md @@ -67,6 +67,7 @@ Accepted fields: | `authorsMapPath` | `string` | `'authors.yml'` | Path to the authors map file, relative to the blog content directory. | | `feedOptions` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. | | `feedOptions.type` | FeedType \| FeedType[] \| 'all' \| null | **Required** | Type of feed to be generated. Use `null` to disable generation. | +| `feedOptions.createFeedItems` | CreateFeedItemsFn \| undefined | `undefined` | An optional function which can be used to transform and / or filter the items in the feed. | | `feedOptions.title` | `string` | `siteConfig.title` | Title of the feed. | | `feedOptions.description` | `string` | \`${siteConfig.title} Blog\` | Description of the feed. | | `feedOptions.copyright` | `string` | `undefined` | Copyright message. | @@ -117,6 +118,17 @@ type ReadingTimeFn = (params: { type FeedType = 'rss' | 'atom' | 'json'; ``` +#### `CreateFeedItemsFn` {#CreateFeedItemsFn} + +```ts +type CreateFeedItemsFn = (params: { + blogPosts: BlogPost[]; + siteConfig: DocusaurusConfig; + outDir: string; + defaultCreateFeedItemsFn: CreateFeedItemsFn; +}) => Promise; +``` + ### Example configuration {#ex-config} You can configure this plugin through preset options or plugin options. @@ -168,6 +180,14 @@ const config = { description: '', copyright: '', language: undefined, + createFeedItems: async (params) => { + const {blogPosts, defaultCreateFeedItems, ...rest} = params; + return defaultCreateFeedItems({ + // keep only the 10 most recent blog posts in the feed + blogPosts: blogPosts.filter((item, index) => index < 10), + ...rest, + }); + }, }, }; ``` diff --git a/website/docs/blog.mdx b/website/docs/blog.mdx index 780a07dbca87..fc5fa935063f 100644 --- a/website/docs/blog.mdx +++ b/website/docs/blog.mdx @@ -511,6 +511,17 @@ type BlogOptions = { description?: string; copyright: string; language?: string; // possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes + /** Allow control over the construction of BlogFeedItems */ + createFeedItems?: (params: { + blogPosts: BlogPost[]; + siteConfig: DocusaurusConfig; + outDir: string; + defaultCreateFeedItems: (params: { + blogPosts: BlogPost[]; + siteConfig: DocusaurusConfig; + outDir: string; + }) => Promise; + }) => Promise; }; }; ``` @@ -529,6 +540,14 @@ module.exports = { feedOptions: { type: 'all', copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, + createFeedItems: async (params) => { + const {blogPosts, defaultCreateFeedItems, ...rest} = params; + return defaultCreateFeedItems({ + // keep only the 10 most recent blog posts in the feed + blogPosts: blogPosts.filter((item, index) => index < 10), + ...rest, + }); + }, }, // highlight-end },