mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-27 06:57:57 +02:00
feat(content-blog): support json feed (#6126)
* 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>
This commit is contained in:
parent
06bd44c693
commit
7e5f6bb805
9 changed files with 165 additions and 30 deletions
|
@ -86,6 +86,86 @@ exports[`blogFeed atom shows feed item for each post 1`] = `
|
||||||
</feed>"
|
</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`] = `
|
exports[`blogFeed rss shows feed item for each post 1`] = `
|
||||||
"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>
|
"<?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/\\">
|
<rss version=\\"2.0\\" xmlns:dc=\\"http://purl.org/dc/elements/1.1/\\" xmlns:content=\\"http://purl.org/rss/1.0/modules/content/\\">
|
||||||
|
|
|
@ -50,7 +50,7 @@ async function testGenerateFeeds(
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('blogFeed', () => {
|
describe('blogFeed', () => {
|
||||||
(['atom', 'rss'] as const).forEach((feedType) => {
|
(['atom', 'rss', 'json'] as const).forEach((feedType) => {
|
||||||
describe(`${feedType}`, () => {
|
describe(`${feedType}`, () => {
|
||||||
test('should not show feed without posts', async () => {
|
test('should not show feed without posts', async () => {
|
||||||
const siteDir = __dirname;
|
const siteDir = __dirname;
|
||||||
|
@ -117,8 +117,22 @@ describe('blogFeed', () => {
|
||||||
defaultReadingTime({content}),
|
defaultReadingTime({content}),
|
||||||
} as PluginOptions,
|
} 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();
|
expect(feedContent).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -78,7 +78,7 @@ test('should convert all feed type to array with other feed type', () => {
|
||||||
});
|
});
|
||||||
expect(value).toEqual({
|
expect(value).toEqual({
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
feedOptions: {type: ['rss', 'atom'], copyright: ''},
|
feedOptions: {type: ['rss', 'atom', 'json'], copyright: ''},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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 {PluginOptions, Author, BlogPost, FeedType} from './types';
|
||||||
import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
|
import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
|
||||||
import {DocusaurusConfig} from '@docusaurus/types';
|
import {DocusaurusConfig} from '@docusaurus/types';
|
||||||
|
@ -68,15 +68,24 @@ export async function generateBlogFeed({
|
||||||
id,
|
id,
|
||||||
metadata: {title: metadataTitle, permalink, date, description, authors},
|
metadata: {title: metadataTitle, permalink, date, description, authors},
|
||||||
} = post;
|
} = post;
|
||||||
feed.addItem({
|
|
||||||
|
const feedItem: FeedItem = {
|
||||||
title: metadataTitle,
|
title: metadataTitle,
|
||||||
id,
|
id,
|
||||||
link: normalizeUrl([siteUrl, permalink]),
|
link: normalizeUrl([siteUrl, permalink]),
|
||||||
date,
|
date,
|
||||||
description,
|
description,
|
||||||
content: mdxToFeedContent(post.content),
|
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;
|
return feed;
|
||||||
|
@ -85,15 +94,26 @@ export async function generateBlogFeed({
|
||||||
async function createBlogFeedFile({
|
async function createBlogFeedFile({
|
||||||
feed,
|
feed,
|
||||||
feedType,
|
feedType,
|
||||||
filePath,
|
generatePath,
|
||||||
}: {
|
}: {
|
||||||
feed: Feed;
|
feed: Feed;
|
||||||
feedType: FeedType;
|
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 {
|
try {
|
||||||
await fs.outputFile(filePath, feedContent);
|
await fs.outputFile(path.join(generatePath, feedPath), feedContent);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Generating ${feedType} feed failed: ${err}.`);
|
throw new Error(`Generating ${feedType} feed failed: ${err}.`);
|
||||||
}
|
}
|
||||||
|
@ -118,12 +138,12 @@ export async function createBlogFeedFiles({
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
feedTypes.map(async (feedType) => {
|
feedTypes.map((feedType) =>
|
||||||
await createBlogFeedFile({
|
createBlogFeedFile({
|
||||||
feed,
|
feed,
|
||||||
feedType,
|
feedType,
|
||||||
filePath: path.join(outDir, options.routeBasePath, `${feedType}.xml`),
|
generatePath: path.join(outDir, options.routeBasePath),
|
||||||
});
|
}),
|
||||||
}),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -550,6 +550,11 @@ export default function pluginContentBlog(
|
||||||
path: 'atom.xml',
|
path: 'atom.xml',
|
||||||
title: `${feedTitle} Atom Feed`,
|
title: `${feedTitle} Atom Feed`,
|
||||||
},
|
},
|
||||||
|
json: {
|
||||||
|
type: 'application/json',
|
||||||
|
path: 'feed.json',
|
||||||
|
title: `${feedTitle} JSON Feed`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const headTags: HtmlTags = [];
|
const headTags: HtmlTags = [];
|
||||||
|
|
||||||
|
|
|
@ -90,12 +90,12 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||||
feedOptions: Joi.object({
|
feedOptions: Joi.object({
|
||||||
type: Joi.alternatives()
|
type: Joi.alternatives()
|
||||||
.try(
|
.try(
|
||||||
Joi.array().items(Joi.string()),
|
Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
|
||||||
Joi.alternatives().conditional(
|
Joi.alternatives().conditional(
|
||||||
Joi.string().equal('all', 'rss', 'atom'),
|
Joi.string().equal('all', 'rss', 'atom', 'json'),
|
||||||
{
|
{
|
||||||
then: Joi.custom((val) =>
|
then: Joi.custom((val) =>
|
||||||
val === 'all' ? ['rss', 'atom'] : [val],
|
val === 'all' ? ['rss', 'atom', 'json'] : [val],
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -24,7 +24,7 @@ export interface BlogContent {
|
||||||
blogTagsListPath: string | null;
|
blogTagsListPath: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeedType = 'rss' | 'atom';
|
export type FeedType = 'rss' | 'atom' | 'json';
|
||||||
|
|
||||||
export type FeedOptions = {
|
export type FeedOptions = {
|
||||||
type?: FeedType[] | null;
|
type?: FeedType[] | null;
|
||||||
|
|
|
@ -56,8 +56,8 @@ Accepted fields:
|
||||||
| `showReadingTime` | `boolean` | `true` | Show estimated reading time for the blog post. |
|
| `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. |
|
| `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. |
|
| `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` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. |
|
||||||
| `feedOptions.type` | <code>'rss' \| 'atom' \| 'all'</code> (or array of multiple options) | **Required** | Type of feed to be generated. |
|
| `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.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. |
|
||||||
|
@ -90,6 +90,8 @@ type ReadingTimeFunctionOption = (params: {
|
||||||
frontMatter: BlogPostFrontMatter & Record<string, unknown>;
|
frontMatter: BlogPostFrontMatter & Record<string, unknown>;
|
||||||
defaultReadingTime: ReadingTimeFunction;
|
defaultReadingTime: ReadingTimeFunction;
|
||||||
}) => number | undefined;
|
}) => number | undefined;
|
||||||
|
|
||||||
|
type FeedType = 'rss' | 'atom' | 'json';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example configuration {#ex-config}
|
## Example configuration {#ex-config}
|
||||||
|
|
|
@ -455,12 +455,14 @@ module.exports = {
|
||||||
|
|
||||||
## Feed {#feed}
|
## 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
|
```ts
|
||||||
|
type FeedType = 'rss' | 'atom' | 'json';
|
||||||
|
|
||||||
type BlogOptions = {
|
type BlogOptions = {
|
||||||
feedOptions?: {
|
feedOptions?: {
|
||||||
type?: 'rss' | 'atom' | 'all' | null;
|
type?: FeedType | 'all' | FeedType[] | null;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
copyright: string;
|
copyright: string;
|
||||||
|
@ -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
|
```text
|
||||||
https://{your-domain}/blog/rss.xml
|
https://example.com/blog/rss.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
and for Atom:
|
</TabItem>
|
||||||
|
<TabItem value="Atom">
|
||||||
|
|
||||||
```text
|
```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}
|
## Advanced topics {#advanced-topics}
|
||||||
|
|
||||||
### Blog-only mode {#blog-only-mode}
|
### Blog-only mode {#blog-only-mode}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue