Skip to content

Commit

Permalink
feat(content-blog): support json feed (#6126)
Browse files Browse the repository at this point in the history
* feat(content-blog): support json feed

* feat(content-blog): support json feed

* feat(content-blog): add json type to default feed options

* Refactors, docs, validation

* Fix test

* Ammend docs

* Add API doc

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
  • Loading branch information
notzheng and Josh-Cena committed Dec 18, 2021
1 parent 06bd44c commit 7e5f6bb
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,86 @@ exports[`blogFeed atom shows feed item for each post 1`] = `
</feed>"
`;
exports[`blogFeed json shows feed item for each post 1`] = `
"{
\\"version\\": \\"https://jsonfeed.org/version/1\\",
\\"title\\": \\"Hello Blog\\",
\\"home_page_url\\": \\"https://docusaurus.io/myBaseUrl/blog\\",
\\"description\\": \\"Hello Blog\\",
\\"items\\": [
{
\\"id\\": \\"/mdx-require-blog-post\\",
\\"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\\"
},
{
\\"id\\": \\"/mdx-blog-post\\",
\\"content_html\\": \\"<h1>HTML Heading 1</h1><h2>HTML Heading 2</h2><p>HTML Paragraph</p><div>Import DOM</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><ul><li>list1</li><li>list2</li><li>list3</li></ul><ul><li>list1</li><li>list2</li><li>list3</li></ul><p>Normal Text <em>Italics Text</em> <strong>Bold Text</strong></p><p><a href=\\\\\\"https://v2.docusaurus.io/\\\\\\">link</a>\\\\n<img src=\\\\\\"https://v2.docusaurus.io/\\\\\\" alt=\\\\\\"image\\\\\\"/></p>\\",
\\"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\\"
},
{
\\"id\\": \\"/hey/my super path/héllô\\",
\\"content_html\\": \\"<p>complex url slug</p>\\",
\\"url\\": \\"https://docusaurus.io/myBaseUrl/blog/hey/my super path/héllô\\",
\\"title\\": \\"Complex Slug\\",
\\"summary\\": \\"complex url slug\\",
\\"date_modified\\": \\"2020-08-16T00:00:00.000Z\\"
},
{
\\"id\\": \\"/simple/slug\\",
\\"content_html\\": \\"<p>simple url slug</p>\\",
\\"url\\": \\"https://docusaurus.io/myBaseUrl/blog/simple/slug\\",
\\"title\\": \\"Simple Slug\\",
\\"summary\\": \\"simple url slug\\",
\\"date_modified\\": \\"2020-08-15T00:00:00.000Z\\",
\\"author\\": {
\\"name\\": \\"Sébastien Lorber\\",
\\"url\\": \\"https://sebastienlorber.com\\"
}
},
{
\\"id\\": \\"/draft\\",
\\"content_html\\": \\"<p>this post should not be published yet</p>\\",
\\"url\\": \\"https://docusaurus.io/myBaseUrl/blog/draft\\",
\\"title\\": \\"draft\\",
\\"summary\\": \\"this post should not be published yet\\",
\\"date_modified\\": \\"2020-02-27T00:00:00.000Z\\"
},
{
\\"id\\": \\"/heading-as-title\\",
\\"content_html\\": \\"\\",
\\"url\\": \\"https://docusaurus.io/myBaseUrl/blog/heading-as-title\\",
\\"title\\": \\"some heading\\",
\\"date_modified\\": \\"2019-01-02T00:00:00.000Z\\"
},
{
\\"id\\": \\"/date-matter\\",
\\"content_html\\": \\"<p>date inside front matter</p>\\",
\\"url\\": \\"https://docusaurus.io/myBaseUrl/blog/date-matter\\",
\\"title\\": \\"date-matter\\",
\\"summary\\": \\"date inside front matter\\",
\\"date_modified\\": \\"2019-01-01T00:00:00.000Z\\"
},
{
\\"id\\": \\"/2018/12/14/Happy-First-Birthday-Slash\\",
\\"content_html\\": \\"<p>Happy birthday! (translated)</p>\\",
\\"url\\": \\"https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash\\",
\\"title\\": \\"Happy 1st Birthday Slash! (translated)\\",
\\"summary\\": \\"Happy birthday! (translated)\\",
\\"date_modified\\": \\"2018-12-14T00:00:00.000Z\\",
\\"author\\": {
\\"name\\": \\"Yangshun Tay (translated)\\"
}
}
]
}"
`;
exports[`blogFeed rss shows feed item for each post 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
<rss version=\\"2.0\\" xmlns:dc=\\"http://purl.org/dc/elements/1.1/\\" xmlns:content=\\"http://purl.org/rss/1.0/modules/content/\\">
Expand Down
20 changes: 17 additions & 3 deletions packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function testGenerateFeeds(
}

describe('blogFeed', () => {
(['atom', 'rss'] as const).forEach((feedType) => {
(['atom', 'rss', 'json'] as const).forEach((feedType) => {
describe(`${feedType}`, () => {
test('should not show feed without posts', async () => {
const siteDir = __dirname;
Expand Down Expand Up @@ -117,8 +117,22 @@ describe('blogFeed', () => {
defaultReadingTime({content}),
} as PluginOptions,
);
const feedContent =
feed && (feedType === 'rss' ? feed.rss2() : feed.atom1());

let feedContent = '';
switch (feedType) {
case 'rss':
feedContent = feed.rss2();
break;
case 'json':
feedContent = feed.json1();
break;
case 'atom':
feedContent = feed.atom1();
break;
default:
break;
}

expect(feedContent).toMatchSnapshot();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test('should convert all feed type to array with other feed type', () => {
});
expect(value).toEqual({
...DEFAULT_OPTIONS,
feedOptions: {type: ['rss', 'atom'], copyright: ''},
feedOptions: {type: ['rss', 'atom', 'json'], copyright: ''},
});
});

Expand Down
46 changes: 33 additions & 13 deletions packages/docusaurus-plugin-content-blog/src/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import {Feed, Author as FeedAuthor} from 'feed';
import {Feed, Author as FeedAuthor, Item as FeedItem} from 'feed';
import {PluginOptions, Author, BlogPost, FeedType} from './types';
import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
import {DocusaurusConfig} from '@docusaurus/types';
Expand Down Expand Up @@ -68,15 +68,24 @@ export async function generateBlogFeed({
id,
metadata: {title: metadataTitle, permalink, date, description, authors},
} = post;
feed.addItem({

const feedItem: FeedItem = {
title: metadataTitle,
id,
link: normalizeUrl([siteUrl, permalink]),
date,
description,
content: mdxToFeedContent(post.content),
author: authors.map(toFeedAuthor),
});
};

// json1() method takes the first item of authors array
// it causes an error when authors array is empty
const feedItemAuthors = authors.map(toFeedAuthor);
if (feedItemAuthors.length > 0) {
feedItem.author = feedItemAuthors;
}

feed.addItem(feedItem);
});

return feed;
Expand All @@ -85,15 +94,26 @@ export async function generateBlogFeed({
async function createBlogFeedFile({
feed,
feedType,
filePath,
generatePath,
}: {
feed: Feed;
feedType: FeedType;
filePath: string;
generatePath: string;
}) {
const feedContent = feedType === 'rss' ? feed.rss2() : feed.atom1();
const [feedContent, feedPath] = (() => {
switch (feedType) {
case 'rss':
return [feed.rss2(), 'rss.xml'];
case 'json':
return [feed.json1(), 'feed.json'];
case 'atom':
return [feed.atom1(), 'atom.xml'];
default:
throw new Error(`Feed type ${feedType} not supported.`);
}
})();
try {
await fs.outputFile(filePath, feedContent);
await fs.outputFile(path.join(generatePath, feedPath), feedContent);
} catch (err) {
throw new Error(`Generating ${feedType} feed failed: ${err}.`);
}
Expand All @@ -118,12 +138,12 @@ export async function createBlogFeedFiles({
}

await Promise.all(
feedTypes.map(async (feedType) => {
await createBlogFeedFile({
feedTypes.map((feedType) =>
createBlogFeedFile({
feed,
feedType,
filePath: path.join(outDir, options.routeBasePath, `${feedType}.xml`),
});
}),
generatePath: path.join(outDir, options.routeBasePath),
}),
),
);
}
5 changes: 5 additions & 0 deletions packages/docusaurus-plugin-content-blog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,11 @@ export default function pluginContentBlog(
path: 'atom.xml',
title: `${feedTitle} Atom Feed`,
},
json: {
type: 'application/json',
path: 'feed.json',
title: `${feedTitle} JSON Feed`,
},
};
const headTags: HtmlTags = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
feedOptions: Joi.object({
type: Joi.alternatives()
.try(
Joi.array().items(Joi.string()),
Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
Joi.alternatives().conditional(
Joi.string().equal('all', 'rss', 'atom'),
Joi.string().equal('all', 'rss', 'atom', 'json'),
{
then: Joi.custom((val) =>
val === 'all' ? ['rss', 'atom'] : [val],
val === 'all' ? ['rss', 'atom', 'json'] : [val],
),
},
),
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-plugin-content-blog/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface BlogContent {
blogTagsListPath: string | null;
}

export type FeedType = 'rss' | 'atom';
export type FeedType = 'rss' | 'atom' | 'json';

export type FeedOptions = {
type?: FeedType[] | null;
Expand Down
6 changes: 4 additions & 2 deletions website/docs/api/plugins/plugin-content-blog.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ Accepted fields:
| `showReadingTime` | `boolean` | `true` | Show estimated reading time for the blog post. |
| `readingTime` | `ReadingTimeFunctionOption` | The default reading time | A callback to customize the reading time number displayed. |
| `authorsMapPath` | `string` | `'authors.yml'` | Path to the authors map file, relative to the blog content directory specified with `path`. Can also be a `json` file. |
| `feedOptions` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. If undefined, no rss feed will be generated. |
| `feedOptions.type` | <code>'rss' \| 'atom' \| 'all'</code> (or array of multiple options) | **Required** | Type of feed to be generated. |
| `feedOptions` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. |
| `feedOptions.type` | <code>FeedType \| FeedType[] \| 'all' \| null</code> | **Required** | Type of feed to be generated. Use `null` to disable generation. |
| `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 @@ -90,6 +90,8 @@ type ReadingTimeFunctionOption = (params: {
frontMatter: BlogPostFrontMatter & Record<string, unknown>;
defaultReadingTime: ReadingTimeFunction;
}) => number | undefined;

type FeedType = 'rss' | 'atom' | 'json';
```

## Example configuration {#ex-config}
Expand Down
28 changes: 21 additions & 7 deletions website/docs/blog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +455,14 @@ module.exports = {

## Feed {#feed}

You can generate RSS/Atom feed by passing feedOptions. By default, RSS and Atom feeds are generated. To disable feed generation, set `feedOptions.type` to `null`.
You can generate RSS / Atom / JSON feed by passing `feedOptions`. By default, RSS and Atom feeds are generated. To disable feed generation, set `feedOptions.type` to `null`.

```ts
type FeedType = 'rss' | 'atom' | 'json';

type BlogOptions = {
feedOptions?: {
type?: 'rss' | 'atom' | 'all' | null;
type?: FeedType | 'all' | FeedType[] | null;
title?: string;
description?: string;
copyright: string;
Expand Down Expand Up @@ -490,20 +492,32 @@ module.exports = {
};
```

Accessing the feed:
The feeds can be found at:

The feed for RSS can be found at:
<Tabs>
<TabItem value="RSS">

```text
https://{your-domain}/blog/rss.xml
https://example.com/blog/rss.xml
```

and for Atom:
</TabItem>
<TabItem value="Atom">

```text
https://{your-domain}/blog/atom.xml
https://example.com/blog/atom.xml
```

</TabItem>
<TabItem value="JSON">

```text
https://example.com/blog/feed.json
```

</TabItem>
</Tabs>

## Advanced topics {#advanced-topics}

### Blog-only mode {#blog-only-mode}
Expand Down

0 comments on commit 7e5f6bb

Please sign in to comment.