@docusaurus/plugin-content-blog 2.0.0-beta.15d451942 → 2.0.0-beta.18

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