@docusaurus/plugin-content-blog 2.0.0-beta.2 → 2.0.0-beta.20

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 (57) hide show
  1. package/lib/authors.d.ts +22 -0
  2. package/lib/authors.js +118 -0
  3. package/lib/blogUtils.d.ts +27 -7
  4. package/lib/blogUtils.js +201 -145
  5. package/lib/feed.d.ts +15 -0
  6. package/lib/feed.js +100 -0
  7. package/lib/frontMatter.d.ts +10 -0
  8. package/lib/{blogFrontMatter.js → frontMatter.js} +31 -19
  9. package/lib/index.d.ts +4 -4
  10. package/lib/index.js +165 -185
  11. package/lib/markdownLoader.d.ts +3 -6
  12. package/lib/markdownLoader.js +6 -7
  13. package/lib/options.d.ts +10 -0
  14. package/lib/{pluginOptionSchema.js → options.js} +43 -13
  15. package/lib/remark/footnoteIDFixer.d.ts +14 -0
  16. package/lib/remark/footnoteIDFixer.js +29 -0
  17. package/lib/translations.d.ts +10 -0
  18. package/lib/translations.js +53 -0
  19. package/lib/types.d.ts +4 -109
  20. package/package.json +22 -18
  21. package/src/authors.ts +164 -0
  22. package/src/blogUtils.ts +306 -204
  23. package/{types.d.ts → src/deps.d.ts} +1 -1
  24. package/src/feed.ts +169 -0
  25. package/src/frontMatter.ts +81 -0
  26. package/src/index.ts +225 -246
  27. package/src/markdownLoader.ts +11 -16
  28. package/src/{pluginOptionSchema.ts → options.ts} +54 -14
  29. package/src/plugin-content-blog.d.ts +580 -0
  30. package/src/remark/footnoteIDFixer.ts +29 -0
  31. package/src/translations.ts +69 -0
  32. package/src/types.ts +2 -128
  33. package/index.d.ts +0 -138
  34. package/lib/.tsbuildinfo +0 -1
  35. package/lib/blogFrontMatter.d.ts +0 -28
  36. package/lib/pluginOptionSchema.d.ts +0 -33
  37. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  38. package/src/__tests__/__fixtures__/website/blog/complex-slug.md +0 -7
  39. package/src/__tests__/__fixtures__/website/blog/date-matter.md +0 -5
  40. package/src/__tests__/__fixtures__/website/blog/draft.md +0 -6
  41. package/src/__tests__/__fixtures__/website/blog/heading-as-title.md +0 -5
  42. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +0 -7
  43. package/src/__tests__/__fixtures__/website/blog-with-ref/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  44. package/src/__tests__/__fixtures__/website/blog-with-ref/post-with-broken-links.md +0 -11
  45. package/src/__tests__/__fixtures__/website/blog-with-ref/post.md +0 -5
  46. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  47. package/src/__tests__/__fixtures__/website-blog-without-date/blog/no date.md +0 -1
  48. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -76
  49. package/src/__tests__/__snapshots__/linkify.test.ts.snap +0 -24
  50. package/src/__tests__/__snapshots__/pluginOptionSchema.test.ts.snap +0 -5
  51. package/src/__tests__/blogFrontMatter.test.ts +0 -317
  52. package/src/__tests__/generateBlogFeed.test.ts +0 -100
  53. package/src/__tests__/index.test.ts +0 -336
  54. package/src/__tests__/linkify.test.ts +0 -93
  55. package/src/__tests__/pluginOptionSchema.test.ts +0 -150
  56. package/src/blogFrontMatter.ts +0 -88
  57. package/tsconfig.json +0 -9
package/src/index.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import fs from 'fs-extra';
9
8
  import path from 'path';
10
9
  import admonitions from 'remark-admonitions';
10
+ import footnoteIDFixer from './remark/footnoteIDFixer';
11
11
  import {
12
12
  normalizeUrl,
13
13
  docuHash,
@@ -16,46 +16,39 @@ import {
16
16
  reportMessage,
17
17
  posixPath,
18
18
  addTrailingPathSeparator,
19
- } from '@docusaurus/utils';
20
- import {
21
- STATIC_DIR_NAME,
19
+ createAbsoluteFilePathMatcher,
20
+ getContentPathList,
21
+ getDataFilePath,
22
22
  DEFAULT_PLUGIN_ID,
23
- } from '@docusaurus/core/lib/constants';
24
- import {flatten, take, kebabCase} from 'lodash';
23
+ type TagsListItem,
24
+ type TagModule,
25
+ } from '@docusaurus/utils';
26
+ import {translateContent, getTranslationFiles} from './translations';
25
27
 
28
+ import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
29
+ import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
26
30
  import {
31
+ generateBlogPosts,
32
+ getSourceToPermalink,
33
+ getBlogTags,
34
+ paginateBlogPosts,
35
+ } from './blogUtils';
36
+ import {createBlogFeedFiles} from './feed';
37
+ import type {
27
38
  PluginOptions,
39
+ BlogPostFrontMatter,
40
+ BlogPostMetadata,
41
+ Assets,
42
+ BlogTag,
28
43
  BlogTags,
29
44
  BlogContent,
30
- BlogItemsToMetadata,
31
- TagsModule,
32
45
  BlogPaginated,
33
- BlogPost,
34
- BlogContentPaths,
35
- BlogMarkdownLoaderOptions,
36
- } from './types';
37
- import {PluginOptionSchema} from './pluginOptionSchema';
38
- import {
39
- LoadContext,
40
- ConfigureWebpackUtils,
41
- Props,
42
- Plugin,
43
- HtmlTags,
44
- OptionValidationContext,
45
- ValidationResult,
46
- } from '@docusaurus/types';
47
- import {Configuration} from 'webpack';
48
- import {
49
- generateBlogFeed,
50
- generateBlogPosts,
51
- getContentPathList,
52
- getSourceToPermalink,
53
- } from './blogUtils';
46
+ } from '@docusaurus/plugin-content-blog';
54
47
 
55
- export default function pluginContentBlog(
48
+ export default async function pluginContentBlog(
56
49
  context: LoadContext,
57
50
  options: PluginOptions,
58
- ): Plugin<BlogContent> {
51
+ ): Promise<Plugin<BlogContent>> {
59
52
  if (options.admonitions) {
60
53
  options.remarkPlugins = options.remarkPlugins.concat([
61
54
  [admonitions, options.admonitions],
@@ -64,10 +57,11 @@ export default function pluginContentBlog(
64
57
 
65
58
  const {
66
59
  siteDir,
67
- siteConfig: {onBrokenMarkdownLinks},
60
+ siteConfig,
68
61
  generatedFilesDir,
69
62
  i18n: {currentLocale},
70
63
  } = context;
64
+ const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
71
65
 
72
66
  const contentPaths: BlogContentPaths = {
73
67
  contentPath: path.resolve(siteDir, options.path),
@@ -88,44 +82,52 @@ export default function pluginContentBlog(
88
82
  const aliasedSource = (source: string) =>
89
83
  `~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
90
84
 
85
+ const authorsMapFilePath = await getDataFilePath({
86
+ filePath: options.authorsMapPath,
87
+ contentPaths,
88
+ });
89
+
91
90
  return {
92
91
  name: 'docusaurus-plugin-content-blog',
93
92
 
94
93
  getPathsToWatch() {
95
- const {include = []} = options;
96
- return flatten(
97
- getContentPathList(contentPaths).map((contentPath) => {
98
- return include.map((pattern) => `${contentPath}/${pattern}`);
99
- }),
94
+ const {include} = options;
95
+ const contentMarkdownGlobs = getContentPathList(contentPaths).flatMap(
96
+ (contentPath) => include.map((pattern) => `${contentPath}/${pattern}`),
100
97
  );
101
- },
102
98
 
103
- getClientModules() {
104
- const modules = [];
105
-
106
- if (options.admonitions) {
107
- modules.push(require.resolve('remark-admonitions/styles/infima.css'));
108
- }
99
+ return [authorsMapFilePath, ...contentMarkdownGlobs].filter(
100
+ Boolean,
101
+ ) as string[];
102
+ },
109
103
 
110
- return modules;
104
+ getTranslationFiles() {
105
+ return getTranslationFiles(options);
111
106
  },
112
107
 
113
108
  // Fetches blog contents and returns metadata for the necessary routes.
114
109
  async loadContent() {
115
- const {postsPerPage, routeBasePath} = options;
110
+ const {
111
+ postsPerPage: postsPerPageOption,
112
+ routeBasePath,
113
+ tagsBasePath,
114
+ blogDescription,
115
+ blogTitle,
116
+ blogSidebarTitle,
117
+ } = options;
116
118
 
117
- const blogPosts: BlogPost[] = await generateBlogPosts(
118
- contentPaths,
119
- context,
120
- options,
121
- );
119
+ const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
120
+ const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
121
+ const blogPosts = await generateBlogPosts(contentPaths, context, options);
122
122
 
123
123
  if (!blogPosts.length) {
124
124
  return {
125
+ blogSidebarTitle,
125
126
  blogPosts: [],
126
127
  blogListPaginated: [],
127
128
  blogTags: {},
128
- blogTagsListPath: null,
129
+ blogTagsListPath,
130
+ blogTagsPaginated: [],
129
131
  };
130
132
  }
131
133
 
@@ -149,85 +151,23 @@ export default function pluginContentBlog(
149
151
  }
150
152
  });
151
153
 
152
- // Blog pagination routes.
153
- // Example: `/blog`, `/blog/page/1`, `/blog/page/2`
154
- const totalCount = blogPosts.length;
155
- const numberOfPages = Math.ceil(totalCount / postsPerPage);
156
- const {
157
- siteConfig: {baseUrl = ''},
158
- } = context;
159
- const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
160
-
161
- const blogListPaginated: BlogPaginated[] = [];
162
-
163
- function blogPaginationPermalink(page: number) {
164
- return page > 0
165
- ? normalizeUrl([basePageUrl, `page/${page + 1}`])
166
- : basePageUrl;
167
- }
168
-
169
- for (let page = 0; page < numberOfPages; page += 1) {
170
- blogListPaginated.push({
171
- metadata: {
172
- permalink: blogPaginationPermalink(page),
173
- page: page + 1,
174
- postsPerPage,
175
- totalPages: numberOfPages,
176
- totalCount,
177
- previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
178
- nextPage:
179
- page < numberOfPages - 1
180
- ? blogPaginationPermalink(page + 1)
181
- : null,
182
- blogDescription: options.blogDescription,
183
- blogTitle: options.blogTitle,
184
- },
185
- items: blogPosts
186
- .slice(page * postsPerPage, (page + 1) * postsPerPage)
187
- .map((item) => item.id),
188
- });
189
- }
190
-
191
- const blogTags: BlogTags = {};
192
- const tagsPath = normalizeUrl([basePageUrl, 'tags']);
193
- blogPosts.forEach((blogPost) => {
194
- const {tags} = blogPost.metadata;
195
- if (!tags || tags.length === 0) {
196
- // TODO: Extract tags out into a separate plugin.
197
- // eslint-disable-next-line no-param-reassign
198
- blogPost.metadata.tags = [];
199
- return;
200
- }
201
-
202
- // eslint-disable-next-line no-param-reassign
203
- blogPost.metadata.tags = tags.map((tag) => {
204
- if (typeof tag === 'string') {
205
- const normalizedTag = kebabCase(tag);
206
- const permalink = normalizeUrl([tagsPath, normalizedTag]);
207
- if (!blogTags[normalizedTag]) {
208
- blogTags[normalizedTag] = {
209
- // Will only use the name of the first occurrence of the tag.
210
- name: tag.toLowerCase(),
211
- items: [],
212
- permalink,
213
- };
214
- }
215
-
216
- blogTags[normalizedTag].items.push(blogPost.id);
217
-
218
- return {
219
- label: tag,
220
- permalink,
221
- };
222
- }
223
- return tag;
224
- });
154
+ const blogListPaginated: BlogPaginated[] = paginateBlogPosts({
155
+ blogPosts,
156
+ blogTitle,
157
+ blogDescription,
158
+ postsPerPageOption,
159
+ basePageUrl: baseBlogUrl,
225
160
  });
226
161
 
227
- const blogTagsListPath =
228
- Object.keys(blogTags).length > 0 ? tagsPath : null;
162
+ const blogTags: BlogTags = getBlogTags({
163
+ blogPosts,
164
+ postsPerPageOption,
165
+ blogDescription,
166
+ blogTitle,
167
+ });
229
168
 
230
169
  return {
170
+ blogSidebarTitle,
231
171
  blogPosts,
232
172
  blogListPaginated,
233
173
  blogTags,
@@ -245,22 +185,47 @@ export default function pluginContentBlog(
245
185
  blogPostComponent,
246
186
  blogTagsListComponent,
247
187
  blogTagsPostsComponent,
188
+ blogArchiveComponent,
189
+ routeBasePath,
190
+ archiveBasePath,
248
191
  } = options;
249
192
 
250
193
  const {addRoute, createData} = actions;
251
194
  const {
195
+ blogSidebarTitle,
252
196
  blogPosts,
253
197
  blogListPaginated,
254
198
  blogTags,
255
199
  blogTagsListPath,
256
200
  } = blogContents;
257
201
 
258
- const blogItemsToMetadata: BlogItemsToMetadata = {};
202
+ const blogItemsToMetadata: {[postId: string]: BlogPostMetadata} = {};
259
203
 
260
204
  const sidebarBlogPosts =
261
205
  options.blogSidebarCount === 'ALL'
262
206
  ? blogPosts
263
- : take(blogPosts, options.blogSidebarCount);
207
+ : blogPosts.slice(0, options.blogSidebarCount);
208
+
209
+ if (archiveBasePath && blogPosts.length) {
210
+ const archiveUrl = normalizeUrl([
211
+ baseUrl,
212
+ routeBasePath,
213
+ archiveBasePath,
214
+ ]);
215
+ // Create a blog archive route
216
+ const archiveProp = await createData(
217
+ `${docuHash(archiveUrl)}.json`,
218
+ JSON.stringify({blogPosts}, null, 2),
219
+ );
220
+ addRoute({
221
+ path: archiveUrl,
222
+ component: blogArchiveComponent,
223
+ exact: true,
224
+ modules: {
225
+ archive: aliasedSource(archiveProp),
226
+ },
227
+ });
228
+ }
264
229
 
265
230
  // This prop is useful to provide the blog list sidebar
266
231
  const sidebarProp = await createData(
@@ -269,7 +234,7 @@ export default function pluginContentBlog(
269
234
  `blog-post-list-prop-${pluginId}.json`,
270
235
  JSON.stringify(
271
236
  {
272
- title: options.blogSidebarTitle,
237
+ title: blogSidebarTitle,
273
238
  items: sidebarBlogPosts.map((blogPost) => ({
274
239
  title: blogPost.metadata.title,
275
240
  permalink: blogPost.metadata.permalink,
@@ -321,77 +286,36 @@ export default function pluginContentBlog(
321
286
  exact: true,
322
287
  modules: {
323
288
  sidebar: aliasedSource(sidebarProp),
324
- items: items.map((postID) => {
325
- // To tell routes.js this is an import and not a nested object to recurse.
326
- return {
327
- content: {
328
- __import: true,
329
- path: blogItemsToMetadata[postID].source,
330
- query: {
331
- truncated: true,
332
- },
289
+ items: items.map((postID) => ({
290
+ content: {
291
+ __import: true,
292
+ path: blogItemsToMetadata[postID]!.source,
293
+ query: {
294
+ truncated: true,
333
295
  },
334
- };
335
- }),
296
+ },
297
+ })),
336
298
  metadata: aliasedSource(pageMetadataPath),
337
299
  },
338
300
  });
339
301
  }),
340
302
  );
341
303
 
342
- // Tags.
343
- if (blogTagsListPath === null) {
304
+ // Tags. This is the last part so we early-return if there are no tags.
305
+ if (Object.keys(blogTags).length === 0) {
344
306
  return;
345
307
  }
346
308
 
347
- const tagsModule: TagsModule = {};
348
-
349
- await Promise.all(
350
- Object.keys(blogTags).map(async (tag) => {
351
- const {name, items, permalink} = blogTags[tag];
352
-
353
- tagsModule[tag] = {
354
- allTagsPath: blogTagsListPath,
355
- slug: tag,
356
- name,
357
- count: items.length,
358
- permalink,
359
- };
360
-
361
- const tagsMetadataPath = await createData(
362
- `${docuHash(permalink)}.json`,
363
- JSON.stringify(tagsModule[tag], null, 2),
364
- );
309
+ async function createTagsListPage() {
310
+ const tagsProp: TagsListItem[] = Object.values(blogTags).map((tag) => ({
311
+ label: tag.label,
312
+ permalink: tag.permalink,
313
+ count: tag.items.length,
314
+ }));
365
315
 
366
- addRoute({
367
- path: permalink,
368
- component: blogTagsPostsComponent,
369
- exact: true,
370
- modules: {
371
- sidebar: aliasedSource(sidebarProp),
372
- items: items.map((postID) => {
373
- const metadata = blogItemsToMetadata[postID];
374
- return {
375
- content: {
376
- __import: true,
377
- path: metadata.source,
378
- query: {
379
- truncated: true,
380
- },
381
- },
382
- };
383
- }),
384
- metadata: aliasedSource(tagsMetadataPath),
385
- },
386
- });
387
- }),
388
- );
389
-
390
- // Only create /tags page if there are tags.
391
- if (Object.keys(blogTags).length > 0) {
392
- const tagsListPath = await createData(
316
+ const tagsPropPath = await createData(
393
317
  `${docuHash(`${blogTagsListPath}-tags`)}.json`,
394
- JSON.stringify(tagsModule, null, 2),
318
+ JSON.stringify(tagsProp, null, 2),
395
319
  );
396
320
 
397
321
  addRoute({
@@ -400,18 +324,66 @@ export default function pluginContentBlog(
400
324
  exact: true,
401
325
  modules: {
402
326
  sidebar: aliasedSource(sidebarProp),
403
- tags: aliasedSource(tagsListPath),
327
+ tags: aliasedSource(tagsPropPath),
404
328
  },
405
329
  });
406
330
  }
331
+
332
+ async function createTagPostsListPage(tag: BlogTag): Promise<void> {
333
+ await Promise.all(
334
+ tag.pages.map(async (blogPaginated) => {
335
+ const {metadata, items} = blogPaginated;
336
+ const tagProp: TagModule = {
337
+ label: tag.label,
338
+ permalink: tag.permalink,
339
+ allTagsPath: blogTagsListPath,
340
+ count: tag.items.length,
341
+ };
342
+ const tagPropPath = await createData(
343
+ `${docuHash(metadata.permalink)}.json`,
344
+ JSON.stringify(tagProp, null, 2),
345
+ );
346
+
347
+ const listMetadataPath = await createData(
348
+ `${docuHash(metadata.permalink)}-list.json`,
349
+ JSON.stringify(metadata, null, 2),
350
+ );
351
+
352
+ addRoute({
353
+ path: metadata.permalink,
354
+ component: blogTagsPostsComponent,
355
+ exact: true,
356
+ modules: {
357
+ sidebar: aliasedSource(sidebarProp),
358
+ items: items.map((postID) => {
359
+ const blogPostMetadata = blogItemsToMetadata[postID]!;
360
+ return {
361
+ content: {
362
+ __import: true,
363
+ path: blogPostMetadata.source,
364
+ query: {
365
+ truncated: true,
366
+ },
367
+ },
368
+ };
369
+ }),
370
+ tag: aliasedSource(tagPropPath),
371
+ listMetadata: aliasedSource(listMetadataPath),
372
+ },
373
+ });
374
+ }),
375
+ );
376
+ }
377
+
378
+ await createTagsListPage();
379
+ await Promise.all(Object.values(blogTags).map(createTagPostsListPage));
407
380
  },
408
381
 
409
- configureWebpack(
410
- _config: Configuration,
411
- isServer: boolean,
412
- {getJSLoader}: ConfigureWebpackUtils,
413
- content,
414
- ) {
382
+ translateContent({content, translationFiles}) {
383
+ return translateContent(content, translationFiles);
384
+ },
385
+
386
+ configureWebpack(_config, isServer, {getJSLoader}, content) {
415
387
  const {
416
388
  rehypePlugins,
417
389
  remarkPlugins,
@@ -436,6 +408,7 @@ export default function pluginContentBlog(
436
408
  },
437
409
  };
438
410
 
411
+ const contentDirs = getContentPathList(contentPaths);
439
412
  return {
440
413
  resolve: {
441
414
  alias: {
@@ -445,8 +418,8 @@ export default function pluginContentBlog(
445
418
  module: {
446
419
  rules: [
447
420
  {
448
- test: /(\.mdx?)$/,
449
- include: getContentPathList(contentPaths)
421
+ test: /\.mdx?$/i,
422
+ include: contentDirs
450
423
  // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
451
424
  .map(addTrailingPathSeparator),
452
425
  use: [
@@ -456,12 +429,22 @@ export default function pluginContentBlog(
456
429
  options: {
457
430
  remarkPlugins,
458
431
  rehypePlugins,
459
- beforeDefaultRemarkPlugins,
432
+ beforeDefaultRemarkPlugins: [
433
+ footnoteIDFixer,
434
+ ...beforeDefaultRemarkPlugins,
435
+ ],
460
436
  beforeDefaultRehypePlugins,
461
- staticDir: path.join(siteDir, STATIC_DIR_NAME),
462
- // Note that metadataPath must be the same/in-sync as
463
- // the path from createData for each MDX.
437
+ staticDirs: siteConfig.staticDirectories.map((dir) =>
438
+ path.resolve(siteDir, dir),
439
+ ),
440
+ siteDir,
441
+ isMDXPartial: createAbsoluteFilePathMatcher(
442
+ options.exclude,
443
+ contentDirs,
444
+ ),
464
445
  metadataPath: (mdxPath: string) => {
446
+ // Note that metadataPath must be the same/in-sync as
447
+ // the path from createData for each MDX.
465
448
  const aliasedPath = aliasedSitePath(mdxPath, siteDir);
466
449
  return path.join(
467
450
  dataDir,
@@ -471,6 +454,21 @@ export default function pluginContentBlog(
471
454
  // For blog posts a title in markdown is always removed
472
455
  // Blog posts title are rendered separately
473
456
  removeContentTitle: true,
457
+
458
+ // Assets allow to convert some relative images paths to
459
+ // require() calls
460
+ createAssets: ({
461
+ frontMatter,
462
+ metadata,
463
+ }: {
464
+ frontMatter: BlogPostFrontMatter;
465
+ metadata: BlogPostMetadata;
466
+ }): Assets => ({
467
+ image: frontMatter.image,
468
+ authorsImageUrls: metadata.authors.map(
469
+ (author) => author.imageURL,
470
+ ),
471
+ }),
474
472
  },
475
473
  },
476
474
  {
@@ -484,34 +482,21 @@ export default function pluginContentBlog(
484
482
  };
485
483
  },
486
484
 
487
- async postBuild({outDir}: Props) {
488
- if (!options.feedOptions?.type) {
485
+ async postBuild({outDir, content}) {
486
+ if (!options.feedOptions.type) {
489
487
  return;
490
488
  }
491
-
492
- const feed = await generateBlogFeed(contentPaths, context, options);
493
-
494
- if (!feed) {
489
+ const {blogPosts} = content;
490
+ if (!blogPosts.length) {
495
491
  return;
496
492
  }
497
-
498
- const feedTypes = options.feedOptions.type;
499
-
500
- await Promise.all(
501
- feedTypes.map(async (feedType) => {
502
- const feedPath = path.join(
503
- outDir,
504
- options.routeBasePath,
505
- `${feedType}.xml`,
506
- );
507
- const feedContent = feedType === 'rss' ? feed.rss2() : feed.atom1();
508
- try {
509
- await fs.outputFile(feedPath, feedContent);
510
- } catch (err) {
511
- throw new Error(`Generating ${feedType} feed failed: ${err}.`);
512
- }
513
- }),
514
- );
493
+ await createBlogFeedFiles({
494
+ blogPosts,
495
+ options,
496
+ outDir,
497
+ siteConfig,
498
+ locale: currentLocale,
499
+ });
515
500
  },
516
501
 
517
502
  injectHtmlTags({content}) {
@@ -524,32 +509,32 @@ export default function pluginContentBlog(
524
509
  }
525
510
 
526
511
  const feedTypes = options.feedOptions.type;
527
- const {
528
- siteConfig: {title},
529
- baseUrl,
530
- } = context;
512
+ const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
531
513
  const feedsConfig = {
532
514
  rss: {
533
515
  type: 'application/rss+xml',
534
516
  path: 'rss.xml',
535
- title: `${title} Blog RSS Feed`,
517
+ title: `${feedTitle} RSS Feed`,
536
518
  },
537
519
  atom: {
538
520
  type: 'application/atom+xml',
539
521
  path: 'atom.xml',
540
- title: `${title} Blog Atom Feed`,
522
+ title: `${feedTitle} Atom Feed`,
523
+ },
524
+ json: {
525
+ type: 'application/json',
526
+ path: 'feed.json',
527
+ title: `${feedTitle} JSON Feed`,
541
528
  },
542
529
  };
543
530
  const headTags: HtmlTags = [];
544
531
 
545
532
  feedTypes.forEach((feedType) => {
546
- const feedConfig = feedsConfig[feedType] || {};
547
-
548
- if (!feedsConfig) {
549
- return;
550
- }
551
-
552
- const {type, path: feedConfigPath, title: feedConfigTitle} = feedConfig;
533
+ const {
534
+ type,
535
+ path: feedConfigPath,
536
+ title: feedConfigTitle,
537
+ } = feedsConfig[feedType];
553
538
 
554
539
  headTags.push({
555
540
  tagName: 'link',
@@ -573,10 +558,4 @@ export default function pluginContentBlog(
573
558
  };
574
559
  }
575
560
 
576
- export function validateOptions({
577
- validate,
578
- options,
579
- }: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
580
- const validatedOptions = validate(PluginOptionSchema, options);
581
- return validatedOptions;
582
- }
561
+ export {validateOptions} from './options';