feat(blog): add options.createFeedItems to filter/limit/transform feed items (#8378)

Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
John Reilly 2022-12-29 12:31:32 +00:00 committed by GitHub
parent 0985fa0af3
commit 022e00554e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 249 additions and 8 deletions

File diff suppressed because one or more lines are too long

View file

@ -143,4 +143,56 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
).toMatchSnapshot(); ).toMatchSnapshot();
fsMock.mockClear(); 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: /<!--\s*truncate\s*-->/,
} as PluginOptions,
);
expect(
fsMock.mock.calls.map((call) => call[1] as string),
).toMatchSnapshot();
fsMock.mockClear();
});
}); });

View file

@ -8,7 +8,7 @@
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import logger from '@docusaurus/logger'; 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 {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
import {blogPostContainerID} from '@docusaurus/utils-common'; import {blogPostContainerID} from '@docusaurus/utils-common';
import {load as cheerioLoad} from 'cheerio'; import {load as cheerioLoad} from 'cheerio';
@ -18,6 +18,7 @@ import type {
PluginOptions, PluginOptions,
Author, Author,
BlogPost, BlogPost,
BlogFeedItem,
} from '@docusaurus/plugin-content-blog'; } from '@docusaurus/plugin-content-blog';
async function generateBlogFeed({ async function generateBlogFeed({
@ -54,11 +55,37 @@ async function generateBlogFeed({
copyright: feedOptions.copyright, 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<BlogFeedItem[]> {
const {url: siteUrl} = siteConfig;
function toFeedAuthor(author: Author): FeedAuthor { function toFeedAuthor(author: Author): FeedAuthor {
return {name: author.name, link: author.url, email: author.email}; return {name: author.name, link: author.url, email: author.email};
} }
await Promise.all( return Promise.all(
blogPosts.map(async (post) => { blogPosts.map(async (post) => {
const { const {
metadata: { metadata: {
@ -79,7 +106,7 @@ async function generateBlogFeed({
const $ = cheerioLoad(content); const $ = cheerioLoad(content);
const link = normalizeUrl([siteUrl, permalink]); const link = normalizeUrl([siteUrl, permalink]);
const feedItem: FeedItem = { const feedItem: BlogFeedItem = {
title: metadataTitle, title: metadataTitle,
id: link, id: link,
link, link,
@ -99,9 +126,7 @@ async function generateBlogFeed({
return feedItem; return feedItem;
}), }),
).then((items) => items.forEach(feed.addItem)); );
return feed;
} }
async function createBlogFeedFile({ async function createBlogFeedFile({

View file

@ -122,6 +122,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
.default(DEFAULT_OPTIONS.feedOptions.copyright), .default(DEFAULT_OPTIONS.feedOptions.copyright),
}), }),
language: Joi.string(), language: Joi.string(),
createFeedItems: Joi.function(),
}).default(DEFAULT_OPTIONS.feedOptions), }).default(DEFAULT_OPTIONS.feedOptions),
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath), authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime), readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),

View file

@ -9,12 +9,19 @@ declare module '@docusaurus/plugin-content-blog' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader'; import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {MDXOptions} from '@docusaurus/mdx-loader'; import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {FrontMatterTag, Tag} from '@docusaurus/utils'; 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'; import type {Overwrite} from 'utility-types';
export type Assets = { 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 * bundler-generated image path. Otherwise, it's empty, and the image URL
* should be accessed through `frontMatter.image`. * should be accessed through `frontMatter.image`.
*/ */
@ -263,6 +270,24 @@ declare module '@docusaurus/plugin-content-blog' {
copyright: string; copyright: string;
/** Language of the feed. */ /** Language of the feed. */
language?: string; language?: string;
/** Allow control over the construction of BlogFeedItems */
createFeedItems?: CreateFeedItemsFn;
};
type DefaultCreateFeedItemsParams = {
blogPosts: BlogPost[];
siteConfig: DocusaurusConfig;
outDir: string;
};
type CreateFeedItemsFn = (
params: CreateFeedItemsParams,
) => Promise<BlogFeedItem[]>;
type CreateFeedItemsParams = DefaultCreateFeedItemsParams & {
defaultCreateFeedItems: (
params: DefaultCreateFeedItemsParams,
) => Promise<BlogFeedItem[]>;
}; };
/** /**
@ -451,6 +476,8 @@ declare module '@docusaurus/plugin-content-blog' {
content: string; content: string;
}; };
export type BlogFeedItem = FeedItem;
export type BlogPaginatedMetadata = { export type BlogPaginatedMetadata = {
/** Title of the entire blog. */ /** Title of the entire blog. */
readonly blogTitle: string; readonly blogTitle: string;

View file

@ -67,6 +67,7 @@ Accepted fields:
| `authorsMapPath` | `string` | `'authors.yml'` | Path to the authors map file, relative to the blog content directory. | | `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` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. |
| `feedOptions.type` | <code><a href="#FeedType">FeedType</a> \| <a href="#FeedType">FeedType</a>[] \| 'all' \| null</code> | **Required** | Type of feed to be generated. Use `null` to disable generation. | | `feedOptions.type` | <code><a href="#FeedType">FeedType</a> \| <a href="#FeedType">FeedType</a>[] \| 'all' \| null</code> | **Required** | Type of feed to be generated. Use `null` to disable generation. |
| `feedOptions.createFeedItems` | <code><a href="#CreateFeedItemsFn">CreateFeedItemsFn</a> \| undefined</code> | `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.title` | `string` | `siteConfig.title` | Title of the feed. |
| `feedOptions.description` | `string` | <code>\`${siteConfig.title} Blog\`</code> | Description of the feed. | | `feedOptions.description` | `string` | <code>\`${siteConfig.title} Blog\`</code> | Description of the feed. |
| `feedOptions.copyright` | `string` | `undefined` | Copyright message. | | `feedOptions.copyright` | `string` | `undefined` | Copyright message. |
@ -117,6 +118,17 @@ type ReadingTimeFn = (params: {
type FeedType = 'rss' | 'atom' | 'json'; type FeedType = 'rss' | 'atom' | 'json';
``` ```
#### `CreateFeedItemsFn` {#CreateFeedItemsFn}
```ts
type CreateFeedItemsFn = (params: {
blogPosts: BlogPost[];
siteConfig: DocusaurusConfig;
outDir: string;
defaultCreateFeedItemsFn: CreateFeedItemsFn;
}) => Promise<BlogFeedItem[]>;
```
### Example configuration {#ex-config} ### Example configuration {#ex-config}
You can configure this plugin through preset options or plugin options. You can configure this plugin through preset options or plugin options.
@ -168,6 +180,14 @@ const config = {
description: '', description: '',
copyright: '', copyright: '',
language: undefined, 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,
});
},
}, },
}; };
``` ```

View file

@ -511,6 +511,17 @@ type BlogOptions = {
description?: string; description?: string;
copyright: string; copyright: string;
language?: string; // possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes 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<BlogFeedItem[]>;
}) => Promise<BlogFeedItem[]>;
}; };
}; };
``` ```
@ -529,6 +540,14 @@ module.exports = {
feedOptions: { feedOptions: {
type: 'all', type: 'all',
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, 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 // highlight-end
}, },