mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 02:08:36 +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>"
|
||||
`;
|
||||
|
||||
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/\\">
|
||||
|
|
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: ''},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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}.`);
|
||||
}
|
||||
|
@ -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),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 = [];
|
||||
|
||||
|
|
|
@ -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],
|
||||
),
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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. |
|
||||
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
@ -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}
|
||||
|
|
Loading…
Add table
Reference in a new issue