Skip to content

Commit

Permalink
feat(blog): add options.createFeedItems to filter/limit/transform fee…
Browse files Browse the repository at this point in the history
…d items (#8378)

Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
  • Loading branch information
johnnyreilly and slorber committed Jan 26, 2023
1 parent 30573dd commit 25a4ec3
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 8 deletions.

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts
Expand Up @@ -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: /<!--\s*truncate\s*-->/,
} as PluginOptions,
);

expect(
fsMock.mock.calls.map((call) => call[1] as string),
).toMatchSnapshot();
fsMock.mockClear();
});
});
37 changes: 31 additions & 6 deletions packages/docusaurus-plugin-content-blog/src/feed.ts
Expand Up @@ -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';
Expand All @@ -18,6 +18,7 @@ import type {
PluginOptions,
Author,
BlogPost,
BlogFeedItem,
} from '@docusaurus/plugin-content-blog';

async function generateBlogFeed({
Expand Down Expand Up @@ -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<BlogFeedItem[]> {
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: {
Expand All @@ -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,
Expand All @@ -99,9 +126,7 @@ async function generateBlogFeed({

return feedItem;
}),
).then((items) => items.forEach(feed.addItem));

return feed;
);
}

async function createBlogFeedFile({
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-plugin-content-blog/src/options.ts
Expand Up @@ -124,6 +124,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
.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),
Expand Down
Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -255,6 +262,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<BlogFeedItem[]>;

type CreateFeedItemsParams = DefaultCreateFeedItemsParams & {
defaultCreateFeedItems: (
params: DefaultCreateFeedItemsParams,
) => Promise<BlogFeedItem[]>;
};

/**
Expand Down Expand Up @@ -436,6 +461,8 @@ declare module '@docusaurus/plugin-content-blog' {
content: string;
};

export type BlogFeedItem = FeedItem;

export type BlogPaginatedMetadata = {
/** Title of the entire blog. */
readonly blogTitle: string;
Expand Down
20 changes: 20 additions & 0 deletions website/docs/api/plugins/plugin-content-blog.md
Expand Up @@ -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` | <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.description` | `string` | <code>\`${siteConfig.title} Blog\`</code> | Description of the feed. |
| `feedOptions.copyright` | `string` | `undefined` | Copyright message. |
Expand Down Expand Up @@ -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<BlogFeedItem[]>;
```

### Example configuration {#ex-config}

You can configure this plugin through preset options or plugin options.
Expand Down Expand Up @@ -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,
});
},
},
};
```
Expand Down
19 changes: 19 additions & 0 deletions website/docs/blog.mdx
Expand Up @@ -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<BlogFeedItem[]>;
}) => Promise<BlogFeedItem[]>;
};
};
```
Expand All @@ -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
},
Expand Down

0 comments on commit 25a4ec3

Please sign in to comment.