@docusaurus/plugin-content-blog 2.0.0-beta.fc64c12e4 → 2.0.0

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