mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-23 22:17:00 +02:00
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:
parent
0985fa0af3
commit
022e00554e
7 changed files with 249 additions and 8 deletions
File diff suppressed because one or more lines are too long
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue