@docusaurus/plugin-content-blog 2.0.0-beta.1decd6f80 → 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 (58) 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 +214 -141
  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/frontMatter.js +62 -0
  9. package/lib/index.d.ts +4 -4
  10. package/lib/index.js +182 -192
  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 -17
  21. package/src/authors.ts +164 -0
  22. package/src/blogUtils.ts +316 -196
  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 +245 -249
  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 -4415
  35. package/lib/blogFrontMatter.d.ts +0 -28
  36. package/lib/blogFrontMatter.js +0 -50
  37. package/lib/pluginOptionSchema.d.ts +0 -33
  38. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  39. package/src/__tests__/__fixtures__/website/blog/complex-slug.md +0 -7
  40. package/src/__tests__/__fixtures__/website/blog/date-matter.md +0 -5
  41. package/src/__tests__/__fixtures__/website/blog/draft.md +0 -6
  42. package/src/__tests__/__fixtures__/website/blog/heading-as-title.md +0 -5
  43. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +0 -7
  44. package/src/__tests__/__fixtures__/website/blog-with-ref/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  45. package/src/__tests__/__fixtures__/website/blog-with-ref/post-with-broken-links.md +0 -11
  46. package/src/__tests__/__fixtures__/website/blog-with-ref/post.md +0 -5
  47. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  48. package/src/__tests__/__fixtures__/website-blog-without-date/blog/no date.md +0 -1
  49. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -101
  50. package/src/__tests__/__snapshots__/linkify.test.ts.snap +0 -24
  51. package/src/__tests__/__snapshots__/pluginOptionSchema.test.ts.snap +0 -5
  52. package/src/__tests__/blogFrontMatter.test.ts +0 -265
  53. package/src/__tests__/generateBlogFeed.test.ts +0 -100
  54. package/src/__tests__/index.test.ts +0 -336
  55. package/src/__tests__/linkify.test.ts +0 -93
  56. package/src/__tests__/pluginOptionSchema.test.ts +0 -150
  57. package/src/blogFrontMatter.ts +0 -87
  58. 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 | null> {
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,37 +82,53 @@ export default function pluginContentBlog(
88
82
  const aliasedSource = (source: string) =>
89
83
  `~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
90
84
 
91
- let blogPosts: BlogPost[] = [];
85
+ const authorsMapFilePath = await getDataFilePath({
86
+ filePath: options.authorsMapPath,
87
+ contentPaths,
88
+ });
92
89
 
93
90
  return {
94
91
  name: 'docusaurus-plugin-content-blog',
95
92
 
96
93
  getPathsToWatch() {
97
- const {include = []} = options;
98
- return flatten(
99
- getContentPathList(contentPaths).map((contentPath) => {
100
- return include.map((pattern) => `${contentPath}/${pattern}`);
101
- }),
94
+ const {include} = options;
95
+ const contentMarkdownGlobs = getContentPathList(contentPaths).flatMap(
96
+ (contentPath) => include.map((pattern) => `${contentPath}/${pattern}`),
102
97
  );
103
- },
104
-
105
- getClientModules() {
106
- const modules = [];
107
98
 
108
- if (options.admonitions) {
109
- modules.push(require.resolve('remark-admonitions/styles/infima.css'));
110
- }
99
+ return [authorsMapFilePath, ...contentMarkdownGlobs].filter(
100
+ Boolean,
101
+ ) as string[];
102
+ },
111
103
 
112
- return modules;
104
+ getTranslationFiles() {
105
+ return getTranslationFiles(options);
113
106
  },
114
107
 
115
108
  // Fetches blog contents and returns metadata for the necessary routes.
116
109
  async loadContent() {
117
- const {postsPerPage, routeBasePath} = options;
110
+ const {
111
+ postsPerPage: postsPerPageOption,
112
+ routeBasePath,
113
+ tagsBasePath,
114
+ blogDescription,
115
+ blogTitle,
116
+ blogSidebarTitle,
117
+ } = options;
118
+
119
+ const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
120
+ const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
121
+ const blogPosts = await generateBlogPosts(contentPaths, context, options);
118
122
 
119
- blogPosts = await generateBlogPosts(contentPaths, context, options);
120
123
  if (!blogPosts.length) {
121
- return null;
124
+ return {
125
+ blogSidebarTitle,
126
+ blogPosts: [],
127
+ blogListPaginated: [],
128
+ blogTags: {},
129
+ blogTagsListPath,
130
+ blogTagsPaginated: [],
131
+ };
122
132
  }
123
133
 
124
134
  // Colocate next and prev metadata.
@@ -141,85 +151,23 @@ export default function pluginContentBlog(
141
151
  }
142
152
  });
143
153
 
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]);
152
-
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
- }
193
-
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
- });
154
+ const blogListPaginated: BlogPaginated[] = paginateBlogPosts({
155
+ blogPosts,
156
+ blogTitle,
157
+ blogDescription,
158
+ postsPerPageOption,
159
+ basePageUrl: baseBlogUrl,
217
160
  });
218
161
 
219
- const blogTagsListPath =
220
- Object.keys(blogTags).length > 0 ? tagsPath : null;
162
+ const blogTags: BlogTags = getBlogTags({
163
+ blogPosts,
164
+ postsPerPageOption,
165
+ blogDescription,
166
+ blogTitle,
167
+ });
221
168
 
222
169
  return {
170
+ blogSidebarTitle,
223
171
  blogPosts,
224
172
  blogListPaginated,
225
173
  blogTags,
@@ -237,22 +185,47 @@ export default function pluginContentBlog(
237
185
  blogPostComponent,
238
186
  blogTagsListComponent,
239
187
  blogTagsPostsComponent,
188
+ blogArchiveComponent,
189
+ routeBasePath,
190
+ archiveBasePath,
240
191
  } = options;
241
192
 
242
193
  const {addRoute, createData} = actions;
243
194
  const {
244
- blogPosts: loadedBlogPosts,
195
+ blogSidebarTitle,
196
+ blogPosts,
245
197
  blogListPaginated,
246
198
  blogTags,
247
199
  blogTagsListPath,
248
200
  } = blogContents;
249
201
 
250
- const blogItemsToMetadata: BlogItemsToMetadata = {};
202
+ const blogItemsToMetadata: {[postId: string]: BlogPostMetadata} = {};
251
203
 
252
204
  const sidebarBlogPosts =
253
205
  options.blogSidebarCount === 'ALL'
254
206
  ? blogPosts
255
- : 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
+ }
256
229
 
257
230
  // This prop is useful to provide the blog list sidebar
258
231
  const sidebarProp = await createData(
@@ -261,7 +234,7 @@ export default function pluginContentBlog(
261
234
  `blog-post-list-prop-${pluginId}.json`,
262
235
  JSON.stringify(
263
236
  {
264
- title: options.blogSidebarTitle,
237
+ title: blogSidebarTitle,
265
238
  items: sidebarBlogPosts.map((blogPost) => ({
266
239
  title: blogPost.metadata.title,
267
240
  permalink: blogPost.metadata.permalink,
@@ -274,7 +247,7 @@ export default function pluginContentBlog(
274
247
 
275
248
  // Create routes for blog entries.
276
249
  await Promise.all(
277
- loadedBlogPosts.map(async (blogPost) => {
250
+ blogPosts.map(async (blogPost) => {
278
251
  const {id, metadata} = blogPost;
279
252
  await createData(
280
253
  // Note that this created data path must be in sync with
@@ -288,7 +261,7 @@ export default function pluginContentBlog(
288
261
  component: blogPostComponent,
289
262
  exact: true,
290
263
  modules: {
291
- sidebar: sidebarProp,
264
+ sidebar: aliasedSource(sidebarProp),
292
265
  content: metadata.source,
293
266
  },
294
267
  });
@@ -312,78 +285,37 @@ export default function pluginContentBlog(
312
285
  component: blogListComponent,
313
286
  exact: true,
314
287
  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 {
319
- content: {
320
- __import: true,
321
- path: blogItemsToMetadata[postID].source,
322
- query: {
323
- truncated: true,
324
- },
288
+ sidebar: aliasedSource(sidebarProp),
289
+ items: items.map((postID) => ({
290
+ content: {
291
+ __import: true,
292
+ path: blogItemsToMetadata[postID]!.source,
293
+ query: {
294
+ truncated: true,
325
295
  },
326
- };
327
- }),
296
+ },
297
+ })),
328
298
  metadata: aliasedSource(pageMetadataPath),
329
299
  },
330
300
  });
331
301
  }),
332
302
  );
333
303
 
334
- // Tags.
335
- 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) {
336
306
  return;
337
307
  }
338
308
 
339
- const tagsModule: TagsModule = {};
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
+ }));
340
315
 
341
- await Promise.all(
342
- Object.keys(blogTags).map(async (tag) => {
343
- const {name, items, permalink} = blogTags[tag];
344
-
345
- tagsModule[tag] = {
346
- allTagsPath: blogTagsListPath,
347
- slug: tag,
348
- name,
349
- count: items.length,
350
- permalink,
351
- };
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
- });
379
- }),
380
- );
381
-
382
- // Only create /tags page if there are tags.
383
- if (Object.keys(blogTags).length > 0) {
384
- const tagsListPath = await createData(
316
+ const tagsPropPath = await createData(
385
317
  `${docuHash(`${blogTagsListPath}-tags`)}.json`,
386
- JSON.stringify(tagsModule, null, 2),
318
+ JSON.stringify(tagsProp, null, 2),
387
319
  );
388
320
 
389
321
  addRoute({
@@ -391,18 +323,67 @@ export default function pluginContentBlog(
391
323
  component: blogTagsListComponent,
392
324
  exact: true,
393
325
  modules: {
394
- sidebar: sidebarProp,
395
- tags: aliasedSource(tagsListPath),
326
+ sidebar: aliasedSource(sidebarProp),
327
+ tags: aliasedSource(tagsPropPath),
396
328
  },
397
329
  });
398
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));
380
+ },
381
+
382
+ translateContent({content, translationFiles}) {
383
+ return translateContent(content, translationFiles);
399
384
  },
400
385
 
401
- configureWebpack(
402
- _config: Configuration,
403
- isServer: boolean,
404
- {getJSLoader}: ConfigureWebpackUtils,
405
- ) {
386
+ configureWebpack(_config, isServer, {getJSLoader}, content) {
406
387
  const {
407
388
  rehypePlugins,
408
389
  remarkPlugins,
@@ -415,7 +396,7 @@ export default function pluginContentBlog(
415
396
  siteDir,
416
397
  contentPaths,
417
398
  truncateMarker,
418
- sourceToPermalink: getSourceToPermalink(blogPosts),
399
+ sourceToPermalink: getSourceToPermalink(content.blogPosts),
419
400
  onBrokenMarkdownLink: (brokenMarkdownLink) => {
420
401
  if (onBrokenMarkdownLinks === 'ignore') {
421
402
  return;
@@ -427,6 +408,7 @@ export default function pluginContentBlog(
427
408
  },
428
409
  };
429
410
 
411
+ const contentDirs = getContentPathList(contentPaths);
430
412
  return {
431
413
  resolve: {
432
414
  alias: {
@@ -436,8 +418,8 @@ export default function pluginContentBlog(
436
418
  module: {
437
419
  rules: [
438
420
  {
439
- test: /(\.mdx?)$/,
440
- include: getContentPathList(contentPaths)
421
+ test: /\.mdx?$/i,
422
+ include: contentDirs
441
423
  // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
442
424
  .map(addTrailingPathSeparator),
443
425
  use: [
@@ -447,18 +429,46 @@ export default function pluginContentBlog(
447
429
  options: {
448
430
  remarkPlugins,
449
431
  rehypePlugins,
450
- beforeDefaultRemarkPlugins,
432
+ beforeDefaultRemarkPlugins: [
433
+ footnoteIDFixer,
434
+ ...beforeDefaultRemarkPlugins,
435
+ ],
451
436
  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.
437
+ staticDirs: siteConfig.staticDirectories.map((dir) =>
438
+ path.resolve(siteDir, dir),
439
+ ),
440
+ siteDir,
441
+ isMDXPartial: createAbsoluteFilePathMatcher(
442
+ options.exclude,
443
+ contentDirs,
444
+ ),
455
445
  metadataPath: (mdxPath: string) => {
446
+ // Note that metadataPath must be the same/in-sync as
447
+ // the path from createData for each MDX.
456
448
  const aliasedPath = aliasedSitePath(mdxPath, siteDir);
457
449
  return path.join(
458
450
  dataDir,
459
451
  `${docuHash(aliasedPath)}.json`,
460
452
  );
461
453
  },
454
+ // For blog posts a title in markdown is always removed
455
+ // Blog posts title are rendered separately
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
+ }),
462
472
  },
463
473
  },
464
474
  {
@@ -472,67 +482,59 @@ export default function pluginContentBlog(
472
482
  };
473
483
  },
474
484
 
475
- async postBuild({outDir}: Props) {
476
- if (!options.feedOptions?.type) {
485
+ async postBuild({outDir, content}) {
486
+ if (!options.feedOptions.type) {
477
487
  return;
478
488
  }
479
-
480
- const feed = await generateBlogFeed(contentPaths, context, options);
481
-
482
- if (!feed) {
489
+ const {blogPosts} = content;
490
+ if (!blogPosts.length) {
483
491
  return;
484
492
  }
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
- );
493
+ await createBlogFeedFiles({
494
+ blogPosts,
495
+ options,
496
+ outDir,
497
+ siteConfig,
498
+ locale: currentLocale,
499
+ });
503
500
  },
504
501
 
505
- injectHtmlTags() {
502
+ injectHtmlTags({content}) {
503
+ if (!content.blogPosts.length) {
504
+ return {};
505
+ }
506
+
506
507
  if (!options.feedOptions?.type) {
507
508
  return {};
508
509
  }
510
+
509
511
  const feedTypes = options.feedOptions.type;
510
- const {
511
- siteConfig: {title},
512
- baseUrl,
513
- } = context;
512
+ const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
514
513
  const feedsConfig = {
515
514
  rss: {
516
515
  type: 'application/rss+xml',
517
516
  path: 'rss.xml',
518
- title: `${title} Blog RSS Feed`,
517
+ title: `${feedTitle} RSS Feed`,
519
518
  },
520
519
  atom: {
521
520
  type: 'application/atom+xml',
522
521
  path: 'atom.xml',
523
- 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`,
524
528
  },
525
529
  };
526
530
  const headTags: HtmlTags = [];
527
531
 
528
532
  feedTypes.forEach((feedType) => {
529
- const feedConfig = feedsConfig[feedType] || {};
530
-
531
- if (!feedsConfig) {
532
- return;
533
- }
534
-
535
- const {type, path: feedConfigPath, title: feedConfigTitle} = feedConfig;
533
+ const {
534
+ type,
535
+ path: feedConfigPath,
536
+ title: feedConfigTitle,
537
+ } = feedsConfig[feedType];
536
538
 
537
539
  headTags.push({
538
540
  tagName: 'link',
@@ -556,10 +558,4 @@ export default function pluginContentBlog(
556
558
  };
557
559
  }
558
560
 
559
- export function validateOptions({
560
- validate,
561
- options,
562
- }: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
563
- const validatedOptions = validate(PluginOptionSchema, options);
564
- return validatedOptions;
565
- }
561
+ export {validateOptions} from './options';