@docusaurus/plugin-content-blog 2.0.0-beta.12faed89d → 2.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/lib/.tsbuildinfo +1 -1
  2. package/lib/authors.d.ts +23 -0
  3. package/lib/authors.js +147 -0
  4. package/lib/blogFrontMatter.d.ts +19 -6
  5. package/lib/blogFrontMatter.js +31 -19
  6. package/lib/blogUtils.d.ts +10 -4
  7. package/lib/blogUtils.js +141 -136
  8. package/lib/feed.d.ts +20 -0
  9. package/lib/feed.js +90 -0
  10. package/lib/index.js +99 -106
  11. package/lib/markdownLoader.d.ts +3 -6
  12. package/lib/markdownLoader.js +5 -5
  13. package/lib/pluginOptionSchema.d.ts +3 -26
  14. package/lib/pluginOptionSchema.js +28 -7
  15. package/lib/translations.d.ts +10 -0
  16. package/lib/translations.js +53 -0
  17. package/lib/types.d.ts +54 -14
  18. package/package.json +17 -13
  19. package/src/__tests__/__fixtures__/authorsMapFiles/authors.json +29 -0
  20. package/src/__tests__/__fixtures__/authorsMapFiles/authors.yml +27 -0
  21. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.json +5 -0
  22. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.yml +3 -0
  23. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.json +3 -0
  24. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.yml +2 -0
  25. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.json +8 -0
  26. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.yml +3 -0
  27. package/src/__tests__/__fixtures__/component/Typography.tsx +6 -0
  28. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathEmpty/empty +0 -0
  29. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson1/authors.json +0 -0
  30. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson2/authors.json +0 -0
  31. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathNestedYml/sub/folder/authors.yml +0 -0
  32. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml1/authors.yml +0 -0
  33. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml2/authors.yml +0 -0
  34. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
  35. package/src/__tests__/__fixtures__/website/blog/_partials/somePartial.md +3 -0
  36. package/src/__tests__/__fixtures__/website/blog/_partials/subfolder/somePartial.md +3 -0
  37. package/src/__tests__/__fixtures__/website/blog/_somePartial.md +3 -0
  38. package/src/__tests__/__fixtures__/website/blog/authors.yml +4 -0
  39. package/src/__tests__/__fixtures__/website/blog/mdx-blog-post.mdx +36 -0
  40. package/src/__tests__/__fixtures__/website/blog/mdx-require-blog-post.mdx +14 -0
  41. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +4 -0
  42. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
  43. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/authors.yml +5 -0
  44. package/src/__tests__/__fixtures__/website/static/img/docusaurus.png +0 -0
  45. package/src/__tests__/__snapshots__/feed.test.ts.snap +164 -0
  46. package/src/__tests__/__snapshots__/translations.test.ts.snap +64 -0
  47. package/src/__tests__/authors.test.ts +608 -0
  48. package/src/__tests__/blogFrontMatter.test.ts +93 -16
  49. package/src/__tests__/blogUtils.test.ts +94 -0
  50. package/src/__tests__/{generateBlogFeed.test.ts → feed.test.ts} +35 -9
  51. package/src/__tests__/index.test.ts +84 -12
  52. package/src/__tests__/pluginOptionSchema.test.ts +3 -3
  53. package/src/__tests__/translations.test.ts +92 -0
  54. package/src/authors.ts +198 -0
  55. package/src/blogFrontMatter.ts +71 -33
  56. package/src/blogUtils.ts +202 -179
  57. package/{types.d.ts → src/deps.d.ts} +0 -0
  58. package/src/feed.ts +129 -0
  59. package/src/index.ts +118 -107
  60. package/src/markdownLoader.ts +8 -12
  61. package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
  62. package/src/pluginOptionSchema.ts +31 -9
  63. package/src/translations.ts +63 -0
  64. package/src/types.ts +69 -16
  65. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -76
package/src/blogUtils.ts CHANGED
@@ -6,18 +6,17 @@
6
6
  */
7
7
 
8
8
  import fs from 'fs-extra';
9
- import globby from 'globby';
10
9
  import chalk from 'chalk';
11
10
  import path from 'path';
12
11
  import readingTime from 'reading-time';
13
- import {Feed} from 'feed';
14
12
  import {keyBy, mapValues} from 'lodash';
15
13
  import {
16
14
  PluginOptions,
17
15
  BlogPost,
18
- DateLink,
19
16
  BlogContentPaths,
20
17
  BlogMarkdownLoaderOptions,
18
+ BlogTags,
19
+ ReadingTimeFunction,
21
20
  } from './types';
22
21
  import {
23
22
  parseMarkdownFile,
@@ -27,9 +26,13 @@ import {
27
26
  getFolderContainingFile,
28
27
  posixPath,
29
28
  replaceMarkdownLinks,
29
+ Globby,
30
+ normalizeFrontMatterTags,
31
+ groupTaggedItems,
30
32
  } from '@docusaurus/utils';
31
33
  import {LoadContext} from '@docusaurus/types';
32
34
  import {validateBlogPostFrontMatter} from './blogFrontMatter';
35
+ import {AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
33
36
 
34
37
  export function truncate(fileString: string, truncateMarker: RegExp): string {
35
38
  return fileString.split(truncateMarker, 1).shift()!;
@@ -44,15 +47,44 @@ export function getSourceToPermalink(
44
47
  );
45
48
  }
46
49
 
47
- // YYYY-MM-DD-{name}.mdx?
48
- // Prefer named capture, but older Node versions do not support it.
49
- const DATE_FILENAME_PATTERN = /^(\d{4}-\d{1,2}-\d{1,2})-?(.*?).mdx?$/;
50
+ export function getBlogTags(blogPosts: BlogPost[]): BlogTags {
51
+ const groups = groupTaggedItems(
52
+ blogPosts,
53
+ (blogPost) => blogPost.metadata.tags,
54
+ );
55
+ return mapValues(groups, (group) => ({
56
+ name: group.tag.label,
57
+ items: group.items.map((item) => item.id),
58
+ permalink: group.tag.permalink,
59
+ }));
60
+ }
50
61
 
51
- function toUrl({date, link}: DateLink) {
52
- return `${date
53
- .toISOString()
54
- .substring(0, '2019-01-01'.length)
55
- .replace(/-/g, '/')}/${link}`;
62
+ const DATE_FILENAME_REGEX =
63
+ /^(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/;
64
+
65
+ type ParsedBlogFileName = {
66
+ date: Date | undefined;
67
+ text: string;
68
+ slug: string;
69
+ };
70
+
71
+ export function parseBlogFileName(
72
+ blogSourceRelative: string,
73
+ ): ParsedBlogFileName {
74
+ const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
75
+ if (dateFilenameMatch) {
76
+ const dateString = dateFilenameMatch.groups!.date!;
77
+ const text = dateFilenameMatch.groups!.text!;
78
+ // Always treat dates as UTC by adding the `Z`
79
+ const date = new Date(`${dateString}Z`);
80
+ const slugDate = dateString.replace(/-/g, '/');
81
+ const slug = `/${slugDate}/${text}`;
82
+ return {date, text, slug};
83
+ } else {
84
+ const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
85
+ const slug = `/${text}`;
86
+ return {date: undefined, text, slug};
87
+ }
56
88
  }
57
89
 
58
90
  function formatBlogPostDate(locale: string, date: Date): string {
@@ -68,209 +100,200 @@ function formatBlogPostDate(locale: string, date: Date): string {
68
100
  }
69
101
  }
70
102
 
71
- export async function generateBlogFeed(
72
- contentPaths: BlogContentPaths,
73
- context: LoadContext,
74
- options: PluginOptions,
75
- ): Promise<Feed | null> {
76
- if (!options.feedOptions) {
77
- throw new Error(
78
- 'Invalid options: "feedOptions" is not expected to be null.',
79
- );
80
- }
81
- const {siteConfig} = context;
82
- const blogPosts = await generateBlogPosts(contentPaths, context, options);
83
- if (!blogPosts.length) {
84
- return null;
85
- }
86
-
87
- const {feedOptions, routeBasePath} = options;
88
- const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
89
- const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
90
-
91
- const updated =
92
- (blogPosts[0] && blogPosts[0].metadata.date) ||
93
- new Date('2015-10-25T16:29:00.000-07:00');
94
-
95
- const feed = new Feed({
96
- id: blogBaseUrl,
97
- title: feedOptions.title || `${title} Blog`,
98
- updated,
99
- language: feedOptions.language,
100
- link: blogBaseUrl,
101
- description: feedOptions.description || `${siteConfig.title} Blog`,
102
- favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
103
- copyright: feedOptions.copyright,
104
- });
105
-
106
- blogPosts.forEach((post) => {
107
- const {
108
- id,
109
- metadata: {title: metadataTitle, permalink, date, description},
110
- } = post;
111
- feed.addItem({
112
- title: metadataTitle,
113
- id,
114
- link: normalizeUrl([siteUrl, permalink]),
115
- date,
116
- description,
117
- });
103
+ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
104
+ const result = await parseMarkdownFile(blogSourceAbsolute, {
105
+ removeContentTitle: true,
118
106
  });
119
-
120
- return feed;
107
+ return {
108
+ ...result,
109
+ frontMatter: validateBlogPostFrontMatter(result.frontMatter),
110
+ };
121
111
  }
122
112
 
123
- export async function generateBlogPosts(
113
+ const defaultReadingTime: ReadingTimeFunction = ({content, options}) =>
114
+ readingTime(content, options).minutes;
115
+
116
+ async function processBlogSourceFile(
117
+ blogSourceRelative: string,
124
118
  contentPaths: BlogContentPaths,
125
- {siteConfig, siteDir, i18n}: LoadContext,
119
+ context: LoadContext,
126
120
  options: PluginOptions,
127
- ): Promise<BlogPost[]> {
121
+ authorsMap?: AuthorsMap,
122
+ ): Promise<BlogPost | undefined> {
123
+ const {
124
+ siteConfig: {baseUrl},
125
+ siteDir,
126
+ i18n,
127
+ } = context;
128
128
  const {
129
- include,
130
129
  routeBasePath,
130
+ tagsBasePath: tagsRouteBasePath,
131
131
  truncateMarker,
132
132
  showReadingTime,
133
133
  editUrl,
134
134
  } = options;
135
135
 
136
- if (!fs.existsSync(contentPaths.contentPath)) {
137
- return [];
138
- }
136
+ // Lookup in localized folder in priority
137
+ const blogDirPath = await getFolderContainingFile(
138
+ getContentPathList(contentPaths),
139
+ blogSourceRelative,
140
+ );
139
141
 
140
- const {baseUrl = ''} = siteConfig;
141
- const blogSourceFiles = await globby(include, {
142
- cwd: contentPaths.contentPath,
143
- });
142
+ const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
143
+
144
+ const {frontMatter, content, contentTitle, excerpt} =
145
+ await parseBlogPostMarkdownFile(blogSourceAbsolute);
144
146
 
145
- const blogPosts: BlogPost[] = [];
147
+ const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
146
148
 
147
- async function processBlogSourceFile(blogSourceFile: string) {
148
- // Lookup in localized folder in priority
149
- const blogDirPath = await getFolderContainingFile(
150
- getContentPathList(contentPaths),
151
- blogSourceFile,
149
+ if (frontMatter.draft && process.env.NODE_ENV === 'production') {
150
+ return undefined;
151
+ }
152
+
153
+ if (frontMatter.id) {
154
+ console.warn(
155
+ chalk.yellow(
156
+ `"id" header option is deprecated in ${blogSourceRelative} file. Please use "slug" option instead.`,
157
+ ),
152
158
  );
159
+ }
153
160
 
154
- const source = path.join(blogDirPath, blogSourceFile);
161
+ const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
155
162
 
156
- const {
157
- frontMatter: unsafeFrontMatter,
158
- content,
159
- contentTitle,
160
- excerpt,
161
- } = await parseMarkdownFile(source, {removeContentTitle: true});
162
- const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter);
163
+ async function getDate(): Promise<Date> {
164
+ // Prefer user-defined date.
165
+ if (frontMatter.date) {
166
+ return new Date(frontMatter.date);
167
+ } else if (parsedBlogFileName.date) {
168
+ return parsedBlogFileName.date;
169
+ }
170
+ // Fallback to file create time
171
+ return (await fs.stat(blogSourceAbsolute)).birthtime;
172
+ }
163
173
 
164
- const aliasedSource = aliasedSitePath(source, siteDir);
174
+ const date = await getDate();
175
+ const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
165
176
 
166
- const blogFileName = path.basename(blogSourceFile);
177
+ const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
178
+ const description = frontMatter.description ?? excerpt ?? '';
167
179
 
168
- if (frontMatter.draft && process.env.NODE_ENV === 'production') {
169
- return;
170
- }
180
+ const slug = frontMatter.slug || parsedBlogFileName.slug;
171
181
 
172
- if (frontMatter.id) {
173
- console.warn(
174
- chalk.yellow(
175
- `"id" header option is deprecated in ${blogFileName} file. Please use "slug" option instead.`,
176
- ),
177
- );
178
- }
182
+ const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
179
183
 
180
- let date: Date | undefined;
181
- // Extract date and title from filename.
182
- const dateFilenameMatch = blogFileName.match(DATE_FILENAME_PATTERN);
183
- let linkName = blogFileName.replace(/\.mdx?$/, '');
184
+ function getBlogEditUrl() {
185
+ const blogPathRelative = path.relative(
186
+ blogDirPath,
187
+ path.resolve(blogSourceAbsolute),
188
+ );
184
189
 
185
- if (dateFilenameMatch) {
186
- const [, dateString, name] = dateFilenameMatch;
187
- // Always treat dates as UTC by adding the `Z`
188
- date = new Date(`${dateString}Z`);
189
- linkName = name;
190
+ if (typeof editUrl === 'function') {
191
+ return editUrl({
192
+ blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
193
+ blogPath: posixPath(blogPathRelative),
194
+ permalink,
195
+ locale: i18n.currentLocale,
196
+ });
197
+ } else if (typeof editUrl === 'string') {
198
+ const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
199
+ const fileContentPath =
200
+ isLocalized && options.editLocalizedFiles
201
+ ? contentPaths.contentPathLocalized
202
+ : contentPaths.contentPath;
203
+
204
+ const contentPathEditUrl = normalizeUrl([
205
+ editUrl,
206
+ posixPath(path.relative(siteDir, fileContentPath)),
207
+ ]);
208
+
209
+ return getEditUrl(blogPathRelative, contentPathEditUrl);
190
210
  }
211
+ return undefined;
212
+ }
191
213
 
192
- // Prefer user-defined date.
193
- if (frontMatter.date) {
194
- date = frontMatter.date;
195
- }
214
+ const tagsBasePath = normalizeUrl([
215
+ baseUrl,
216
+ routeBasePath,
217
+ tagsRouteBasePath,
218
+ ]);
219
+ const authors = getBlogPostAuthors({authorsMap, frontMatter});
220
+
221
+ return {
222
+ id: slug,
223
+ metadata: {
224
+ permalink,
225
+ editUrl: getBlogEditUrl(),
226
+ source: aliasedSource,
227
+ title,
228
+ description,
229
+ date,
230
+ formattedDate,
231
+ tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
232
+ readingTime: showReadingTime
233
+ ? options.readingTime({
234
+ content,
235
+ frontMatter,
236
+ defaultReadingTime,
237
+ })
238
+ : undefined,
239
+ truncated: truncateMarker?.test(content) || false,
240
+ authors,
241
+ },
242
+ content,
243
+ };
244
+ }
196
245
 
197
- // Use file create time for blog.
198
- date = date ?? (await fs.stat(source)).birthtime;
199
- const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
200
-
201
- const title = frontMatter.title ?? contentTitle ?? linkName;
202
- const description = frontMatter.description ?? excerpt ?? '';
203
-
204
- const slug =
205
- frontMatter.slug ||
206
- (dateFilenameMatch ? toUrl({date, link: linkName}) : linkName);
207
-
208
- const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
209
-
210
- function getBlogEditUrl() {
211
- const blogPathRelative = path.relative(blogDirPath, path.resolve(source));
212
-
213
- if (typeof editUrl === 'function') {
214
- return editUrl({
215
- blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
216
- blogPath: posixPath(blogPathRelative),
217
- permalink,
218
- locale: i18n.currentLocale,
219
- });
220
- } else if (typeof editUrl === 'string') {
221
- const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
222
- const fileContentPath =
223
- isLocalized && options.editLocalizedFiles
224
- ? contentPaths.contentPathLocalized
225
- : contentPaths.contentPath;
226
-
227
- const contentPathEditUrl = normalizeUrl([
228
- editUrl,
229
- posixPath(path.relative(siteDir, fileContentPath)),
230
- ]);
231
-
232
- return getEditUrl(blogPathRelative, contentPathEditUrl);
233
- } else {
234
- return undefined;
235
- }
236
- }
246
+ export async function generateBlogPosts(
247
+ contentPaths: BlogContentPaths,
248
+ context: LoadContext,
249
+ options: PluginOptions,
250
+ ): Promise<BlogPost[]> {
251
+ const {include, exclude} = options;
237
252
 
238
- blogPosts.push({
239
- id: frontMatter.slug ?? title,
240
- metadata: {
241
- permalink,
242
- editUrl: getBlogEditUrl(),
243
- source: aliasedSource,
244
- title,
245
- description,
246
- date,
247
- formattedDate,
248
- tags: frontMatter.tags ?? [],
249
- readingTime: showReadingTime ? readingTime(content).minutes : undefined,
250
- truncated: truncateMarker?.test(content) || false,
251
- },
252
- });
253
+ if (!fs.existsSync(contentPaths.contentPath)) {
254
+ return [];
253
255
  }
254
256
 
255
- await Promise.all(
256
- blogSourceFiles.map(async (blogSourceFile: string) => {
257
- try {
258
- return await processBlogSourceFile(blogSourceFile);
259
- } catch (e) {
260
- console.error(
261
- chalk.red(
262
- `Processing of blog source file failed for path "${blogSourceFile}"`,
263
- ),
264
- );
265
- throw e;
266
- }
267
- }),
268
- );
257
+ const blogSourceFiles = await Globby(include, {
258
+ cwd: contentPaths.contentPath,
259
+ ignore: exclude,
260
+ });
261
+
262
+ const authorsMap = await getAuthorsMap({
263
+ contentPaths,
264
+ authorsMapPath: options.authorsMapPath,
265
+ });
266
+
267
+ const blogPosts = (
268
+ await Promise.all(
269
+ blogSourceFiles.map(async (blogSourceFile: string) => {
270
+ try {
271
+ return await processBlogSourceFile(
272
+ blogSourceFile,
273
+ contentPaths,
274
+ context,
275
+ options,
276
+ authorsMap,
277
+ );
278
+ } catch (e) {
279
+ console.error(
280
+ chalk.red(
281
+ `Processing of blog source file failed for path "${blogSourceFile}"`,
282
+ ),
283
+ );
284
+ throw e;
285
+ }
286
+ }),
287
+ )
288
+ ).filter(Boolean) as BlogPost[];
269
289
 
270
290
  blogPosts.sort(
271
291
  (a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(),
272
292
  );
273
293
 
294
+ if (options.sortPosts === 'ascending') {
295
+ return blogPosts.reverse();
296
+ }
274
297
  return blogPosts;
275
298
  }
276
299
 
File without changes
package/src/feed.ts ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import {Feed, Author as FeedAuthor} from 'feed';
9
+ import {PluginOptions, Author, BlogPost, FeedType} from './types';
10
+ import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
11
+ import {DocusaurusConfig} from '@docusaurus/types';
12
+ import path from 'path';
13
+ import fs from 'fs-extra';
14
+
15
+ // TODO this is temporary until we handle mdxToHtml better
16
+ // It's hard to convert reliably JSX/require calls to an html feed content
17
+ // See https://github.com/facebook/docusaurus/issues/5664
18
+ function mdxToFeedContent(mdxContent: string): string | undefined {
19
+ try {
20
+ return mdxToHtml(mdxContent);
21
+ } catch (e) {
22
+ // TODO will we need a plugin option to configure how to handle such an error
23
+ // Swallow the error on purpose for now, until we understand better the problem space
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ export async function generateBlogFeed({
29
+ blogPosts,
30
+ options,
31
+ siteConfig,
32
+ }: {
33
+ blogPosts: BlogPost[];
34
+ options: PluginOptions;
35
+ siteConfig: DocusaurusConfig;
36
+ }): Promise<Feed | null> {
37
+ if (!blogPosts.length) {
38
+ return null;
39
+ }
40
+
41
+ const {feedOptions, routeBasePath} = options;
42
+ const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
43
+ const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
44
+
45
+ const updated =
46
+ (blogPosts[0] && blogPosts[0].metadata.date) ||
47
+ new Date('2015-10-25T16:29:00.000-07:00'); // weird legacy magic date
48
+
49
+ const feed = new Feed({
50
+ id: blogBaseUrl,
51
+ title: feedOptions.title || `${title} Blog`,
52
+ updated,
53
+ language: feedOptions.language,
54
+ link: blogBaseUrl,
55
+ description: feedOptions.description || `${siteConfig.title} Blog`,
56
+ favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
57
+ copyright: feedOptions.copyright,
58
+ });
59
+
60
+ function toFeedAuthor(author: Author): FeedAuthor {
61
+ // TODO ask author emails?
62
+ // RSS feed requires email to render authors
63
+ return {name: author.name, link: author.url};
64
+ }
65
+
66
+ blogPosts.forEach((post) => {
67
+ const {
68
+ id,
69
+ metadata: {title: metadataTitle, permalink, date, description, authors},
70
+ } = post;
71
+ feed.addItem({
72
+ title: metadataTitle,
73
+ id,
74
+ link: normalizeUrl([siteUrl, permalink]),
75
+ date,
76
+ description,
77
+ content: mdxToFeedContent(post.content),
78
+ author: authors.map(toFeedAuthor),
79
+ });
80
+ });
81
+
82
+ return feed;
83
+ }
84
+
85
+ async function createBlogFeedFile({
86
+ feed,
87
+ feedType,
88
+ filePath,
89
+ }: {
90
+ feed: Feed;
91
+ feedType: FeedType;
92
+ filePath: string;
93
+ }) {
94
+ const feedContent = feedType === 'rss' ? feed.rss2() : feed.atom1();
95
+ try {
96
+ await fs.outputFile(filePath, feedContent);
97
+ } catch (err) {
98
+ throw new Error(`Generating ${feedType} feed failed: ${err}.`);
99
+ }
100
+ }
101
+
102
+ export async function createBlogFeedFiles({
103
+ blogPosts,
104
+ options,
105
+ siteConfig,
106
+ outDir,
107
+ }: {
108
+ blogPosts: BlogPost[];
109
+ options: PluginOptions;
110
+ siteConfig: DocusaurusConfig;
111
+ outDir: string;
112
+ }): Promise<void> {
113
+ const feed = await generateBlogFeed({blogPosts, options, siteConfig});
114
+
115
+ const feedTypes = options.feedOptions.type;
116
+ if (!feed || !feedTypes) {
117
+ return;
118
+ }
119
+
120
+ await Promise.all(
121
+ feedTypes.map(async (feedType) => {
122
+ await createBlogFeedFile({
123
+ feed,
124
+ feedType,
125
+ filePath: path.join(outDir, options.routeBasePath, `${feedType}.xml`),
126
+ });
127
+ }),
128
+ );
129
+ }