diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap index e73f5db72049..1571085f5c72 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__snapshots__/feed.test.ts.snap @@ -86,6 +86,86 @@ exports[`blogFeed atom shows feed item for each post 1`] = ` " `; +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\\": \\"

HTML Heading 1

HTML Heading 2

HTML Paragraph

Import DOM

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5

Normal Text Italics Text Bold Text

link\\\\n\\\\\\"image\\\\\\"/

\\", + \\"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\\": \\"

complex url slug

\\", + \\"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\\": \\"

simple url slug

\\", + \\"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\\": \\"

this post should not be published yet

\\", + \\"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\\": \\"

date inside front matter

\\", + \\"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\\": \\"

Happy birthday! (translated)

\\", + \\"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`] = ` " diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts index 2c2d0337a46e..6493aa1bf734 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -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(); }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/pluginOptionSchema.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/pluginOptionSchema.test.ts index 78663ce02238..4c0fdef73f16 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/pluginOptionSchema.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/pluginOptionSchema.test.ts @@ -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: ''}, }); }); diff --git a/packages/docusaurus-plugin-content-blog/src/feed.ts b/packages/docusaurus-plugin-content-blog/src/feed.ts index b438fcecd1b6..659141b67979 100644 --- a/packages/docusaurus-plugin-content-blog/src/feed.ts +++ b/packages/docusaurus-plugin-content-blog/src/feed.ts @@ -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), + }), + ), ); } diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 723b048bda3a..9b9e98fb870e 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -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 = []; diff --git a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts index e231ea36bf2a..ff60adc9c94e 100644 --- a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts +++ b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts @@ -90,12 +90,12 @@ export const PluginOptionSchema = Joi.object({ 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], ), }, ), diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index 15d129bcca67..76fbacc95980 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -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; diff --git a/website/docs/api/plugins/plugin-content-blog.md b/website/docs/api/plugins/plugin-content-blog.md index 06d7bdca5b5f..1d325ce3c198 100644 --- a/website/docs/api/plugins/plugin-content-blog.md +++ b/website/docs/api/plugins/plugin-content-blog.md @@ -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` | 'rss' \| 'atom' \| 'all' (or array of multiple options) | **Required** | Type of feed to be generated. | +| `feedOptions` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. | +| `feedOptions.type` | FeedType \| FeedType[] \| 'all' \| null | **Required** | Type of feed to be generated. Use `null` to disable generation. | | `feedOptions.title` | `string` | `siteConfig.title` | Title of the feed. | | `feedOptions.description` | `string` | \`${siteConfig.title} Blog\` | Description of the feed. | | `feedOptions.copyright` | `string` | `undefined` | Copyright message. | @@ -90,6 +90,8 @@ type ReadingTimeFunctionOption = (params: { frontMatter: BlogPostFrontMatter & Record; defaultReadingTime: ReadingTimeFunction; }) => number | undefined; + +type FeedType = 'rss' | 'atom' | 'json'; ``` ## Example configuration {#ex-config} diff --git a/website/docs/blog.mdx b/website/docs/blog.mdx index 171cc12c7607..035732b7ede9 100644 --- a/website/docs/blog.mdx +++ b/website/docs/blog.mdx @@ -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: + + ```text -https://{your-domain}/blog/rss.xml +https://example.com/blog/rss.xml ``` -and for Atom: + + ```text -https://{your-domain}/blog/atom.xml +https://example.com/blog/atom.xml ``` + + + +```text +https://example.com/blog/feed.json +``` + + + + ## Advanced topics {#advanced-topics} ### Blog-only mode {#blog-only-mode}