@docusaurus/plugin-content-blog 2.0.0-beta.fbdeefcac → 2.0.0-rc.1

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 +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 +165 -201
  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} +44 -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 -109
  20. package/package.json +23 -19
  21. package/src/authors.ts +168 -0
  22. package/src/blogUtils.ts +306 -204
  23. package/src/feed.ts +171 -0
  24. package/src/frontMatter.ts +81 -0
  25. package/src/index.ts +230 -269
  26. package/src/markdownLoader.ts +11 -16
  27. package/src/{pluginOptionSchema.ts → options.ts} +57 -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 -128
  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 -33
  36. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  37. package/src/__tests__/__fixtures__/website/blog/complex-slug.md +0 -7
  38. package/src/__tests__/__fixtures__/website/blog/date-matter.md +0 -5
  39. package/src/__tests__/__fixtures__/website/blog/draft.md +0 -6
  40. package/src/__tests__/__fixtures__/website/blog/heading-as-title.md +0 -5
  41. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +0 -7
  42. package/src/__tests__/__fixtures__/website/blog-with-ref/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  43. package/src/__tests__/__fixtures__/website/blog-with-ref/post-with-broken-links.md +0 -11
  44. package/src/__tests__/__fixtures__/website/blog-with-ref/post.md +0 -5
  45. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  46. package/src/__tests__/__fixtures__/website-blog-without-date/blog/no date.md +0 -1
  47. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -76
  48. package/src/__tests__/__snapshots__/linkify.test.ts.snap +0 -24
  49. package/src/__tests__/__snapshots__/pluginOptionSchema.test.ts.snap +0 -5
  50. package/src/__tests__/blogFrontMatter.test.ts +0 -317
  51. package/src/__tests__/generateBlogFeed.test.ts +0 -100
  52. package/src/__tests__/index.test.ts +0 -336
  53. package/src/__tests__/linkify.test.ts +0 -93
  54. package/src/__tests__/pluginOptionSchema.test.ts +0 -150
  55. package/src/blogFrontMatter.ts +0 -88
  56. package/tsconfig.json +0 -9
  57. package/types.d.ts +0 -13
package/src/index.ts CHANGED
@@ -5,75 +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,
17
+ createAbsoluteFilePathMatcher,
18
+ getContentPathList,
19
+ getDataFilePath,
20
+ DEFAULT_PLUGIN_ID,
21
+ type TagsListItem,
22
+ type TagModule,
19
23
  } from '@docusaurus/utils';
20
24
  import {
21
- STATIC_DIR_NAME,
22
- DEFAULT_PLUGIN_ID,
23
- } from '@docusaurus/core/lib/constants';
24
- 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';
25
33
 
26
- import {
34
+ import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
35
+ import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
36
+ import type {
27
37
  PluginOptions,
38
+ BlogPostFrontMatter,
39
+ BlogPostMetadata,
40
+ Assets,
41
+ BlogTag,
28
42
  BlogTags,
29
43
  BlogContent,
30
- BlogItemsToMetadata,
31
- TagsModule,
32
44
  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';
45
+ } from '@docusaurus/plugin-content-blog';
54
46
 
55
- export default function pluginContentBlog(
47
+ export default async function pluginContentBlog(
56
48
  context: LoadContext,
57
49
  options: PluginOptions,
58
- ): Plugin<BlogContent> {
59
- if (options.admonitions) {
60
- options.remarkPlugins = options.remarkPlugins.concat([
61
- [admonitions, options.admonitions],
62
- ]);
63
- }
64
-
50
+ ): Promise<Plugin<BlogContent>> {
65
51
  const {
66
52
  siteDir,
67
- siteConfig: {onBrokenMarkdownLinks},
53
+ siteConfig,
68
54
  generatedFilesDir,
55
+ localizationDir,
69
56
  i18n: {currentLocale},
70
57
  } = context;
58
+ const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
71
59
 
72
60
  const contentPaths: BlogContentPaths = {
73
61
  contentPath: path.resolve(siteDir, options.path),
74
62
  contentPathLocalized: getPluginI18nPath({
75
- siteDir,
76
- locale: currentLocale,
63
+ localizationDir,
77
64
  pluginName: 'docusaurus-plugin-content-blog',
78
65
  pluginId: options.id,
79
66
  }),
@@ -88,44 +75,52 @@ export default function pluginContentBlog(
88
75
  const aliasedSource = (source: string) =>
89
76
  `~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
90
77
 
78
+ const authorsMapFilePath = await getDataFilePath({
79
+ filePath: options.authorsMapPath,
80
+ contentPaths,
81
+ });
82
+
91
83
  return {
92
84
  name: 'docusaurus-plugin-content-blog',
93
85
 
94
86
  getPathsToWatch() {
95
- const {include = []} = options;
96
- return flatten(
97
- getContentPathList(contentPaths).map((contentPath) => {
98
- return include.map((pattern) => `${contentPath}/${pattern}`);
99
- }),
87
+ const {include} = options;
88
+ const contentMarkdownGlobs = getContentPathList(contentPaths).flatMap(
89
+ (contentPath) => include.map((pattern) => `${contentPath}/${pattern}`),
100
90
  );
101
- },
102
-
103
- getClientModules() {
104
- const modules = [];
105
91
 
106
- if (options.admonitions) {
107
- modules.push(require.resolve('remark-admonitions/styles/infima.css'));
108
- }
92
+ return [authorsMapFilePath, ...contentMarkdownGlobs].filter(
93
+ Boolean,
94
+ ) as string[];
95
+ },
109
96
 
110
- return modules;
97
+ getTranslationFiles() {
98
+ return getTranslationFiles(options);
111
99
  },
112
100
 
113
101
  // Fetches blog contents and returns metadata for the necessary routes.
114
102
  async loadContent() {
115
- const {postsPerPage, routeBasePath} = options;
103
+ const {
104
+ postsPerPage: postsPerPageOption,
105
+ routeBasePath,
106
+ tagsBasePath,
107
+ blogDescription,
108
+ blogTitle,
109
+ blogSidebarTitle,
110
+ } = options;
116
111
 
117
- const blogPosts: BlogPost[] = await generateBlogPosts(
118
- contentPaths,
119
- context,
120
- options,
121
- );
112
+ const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
113
+ const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
114
+ const blogPosts = await generateBlogPosts(contentPaths, context, options);
122
115
 
123
116
  if (!blogPosts.length) {
124
117
  return {
118
+ blogSidebarTitle,
125
119
  blogPosts: [],
126
120
  blogListPaginated: [],
127
121
  blogTags: {},
128
- blogTagsListPath: null,
122
+ blogTagsListPath,
123
+ blogTagsPaginated: [],
129
124
  };
130
125
  }
131
126
 
@@ -149,85 +144,23 @@ export default function pluginContentBlog(
149
144
  }
150
145
  });
151
146
 
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
- });
147
+ const blogListPaginated: BlogPaginated[] = paginateBlogPosts({
148
+ blogPosts,
149
+ blogTitle,
150
+ blogDescription,
151
+ postsPerPageOption,
152
+ basePageUrl: baseBlogUrl,
225
153
  });
226
154
 
227
- const blogTagsListPath =
228
- Object.keys(blogTags).length > 0 ? tagsPath : null;
155
+ const blogTags: BlogTags = getBlogTags({
156
+ blogPosts,
157
+ postsPerPageOption,
158
+ blogDescription,
159
+ blogTitle,
160
+ });
229
161
 
230
162
  return {
163
+ blogSidebarTitle,
231
164
  blogPosts,
232
165
  blogListPaginated,
233
166
  blogTags,
@@ -236,31 +169,67 @@ export default function pluginContentBlog(
236
169
  },
237
170
 
238
171
  async contentLoaded({content: blogContents, actions}) {
239
- if (!blogContents) {
240
- return;
241
- }
242
-
243
172
  const {
244
173
  blogListComponent,
245
174
  blogPostComponent,
246
175
  blogTagsListComponent,
247
176
  blogTagsPostsComponent,
177
+ blogArchiveComponent,
178
+ routeBasePath,
179
+ archiveBasePath,
248
180
  } = options;
249
181
 
250
182
  const {addRoute, createData} = actions;
251
183
  const {
184
+ blogSidebarTitle,
252
185
  blogPosts,
253
186
  blogListPaginated,
254
187
  blogTags,
255
188
  blogTagsListPath,
256
189
  } = blogContents;
257
190
 
258
- const blogItemsToMetadata: BlogItemsToMetadata = {};
191
+ const blogItemsToMetadata: {[postId: string]: BlogPostMetadata} = {};
259
192
 
260
193
  const sidebarBlogPosts =
261
194
  options.blogSidebarCount === 'ALL'
262
195
  ? blogPosts
263
- : 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
+ }
264
233
 
265
234
  // This prop is useful to provide the blog list sidebar
266
235
  const sidebarProp = await createData(
@@ -269,7 +238,7 @@ export default function pluginContentBlog(
269
238
  `blog-post-list-prop-${pluginId}.json`,
270
239
  JSON.stringify(
271
240
  {
272
- title: options.blogSidebarTitle,
241
+ title: blogSidebarTitle,
273
242
  items: sidebarBlogPosts.map((blogPost) => ({
274
243
  title: blogPost.metadata.title,
275
244
  permalink: blogPost.metadata.permalink,
@@ -321,77 +290,28 @@ export default function pluginContentBlog(
321
290
  exact: true,
322
291
  modules: {
323
292
  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
- },
333
- },
334
- };
335
- }),
293
+ items: blogPostItemsModule(items),
336
294
  metadata: aliasedSource(pageMetadataPath),
337
295
  },
338
296
  });
339
297
  }),
340
298
  );
341
299
 
342
- // Tags.
343
- 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) {
344
302
  return;
345
303
  }
346
304
 
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
- );
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
+ }));
365
311
 
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(
312
+ const tagsPropPath = await createData(
393
313
  `${docuHash(`${blogTagsListPath}-tags`)}.json`,
394
- JSON.stringify(tagsModule, null, 2),
314
+ JSON.stringify(tagsProp, null, 2),
395
315
  );
396
316
 
397
317
  addRoute({
@@ -400,19 +320,57 @@ export default function pluginContentBlog(
400
320
  exact: true,
401
321
  modules: {
402
322
  sidebar: aliasedSource(sidebarProp),
403
- tags: aliasedSource(tagsListPath),
323
+ tags: aliasedSource(tagsPropPath),
404
324
  },
405
325
  });
406
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));
407
365
  },
408
366
 
409
- configureWebpack(
410
- _config: Configuration,
411
- isServer: boolean,
412
- {getJSLoader}: ConfigureWebpackUtils,
413
- content,
414
- ) {
367
+ translateContent({content, translationFiles}) {
368
+ return translateContent(content, translationFiles);
369
+ },
370
+
371
+ configureWebpack(_config, isServer, {getJSLoader}, content) {
415
372
  const {
373
+ admonitions,
416
374
  rehypePlugins,
417
375
  remarkPlugins,
418
376
  truncateMarker,
@@ -429,13 +387,13 @@ export default function pluginContentBlog(
429
387
  if (onBrokenMarkdownLinks === 'ignore') {
430
388
  return;
431
389
  }
432
- reportMessage(
433
- `Blog markdown link couldn't be resolved: (${brokenMarkdownLink.link}) in ${brokenMarkdownLink.filePath}`,
390
+ logger.report(
434
391
  onBrokenMarkdownLinks,
435
- );
392
+ )`Blog markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath}`;
436
393
  },
437
394
  };
438
395
 
396
+ const contentDirs = getContentPathList(contentPaths);
439
397
  return {
440
398
  resolve: {
441
399
  alias: {
@@ -445,8 +403,8 @@ export default function pluginContentBlog(
445
403
  module: {
446
404
  rules: [
447
405
  {
448
- test: /(\.mdx?)$/,
449
- include: getContentPathList(contentPaths)
406
+ test: /\.mdx?$/i,
407
+ include: contentDirs
450
408
  // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
451
409
  .map(addTrailingPathSeparator),
452
410
  use: [
@@ -454,14 +412,25 @@ export default function pluginContentBlog(
454
412
  {
455
413
  loader: require.resolve('@docusaurus/mdx-loader'),
456
414
  options: {
415
+ admonitions,
457
416
  remarkPlugins,
458
417
  rehypePlugins,
459
- beforeDefaultRemarkPlugins,
418
+ beforeDefaultRemarkPlugins: [
419
+ footnoteIDFixer,
420
+ ...beforeDefaultRemarkPlugins,
421
+ ],
460
422
  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.
423
+ staticDirs: siteConfig.staticDirectories.map((dir) =>
424
+ path.resolve(siteDir, dir),
425
+ ),
426
+ siteDir,
427
+ isMDXPartial: createAbsoluteFilePathMatcher(
428
+ options.exclude,
429
+ contentDirs,
430
+ ),
464
431
  metadataPath: (mdxPath: string) => {
432
+ // Note that metadataPath must be the same/in-sync as
433
+ // the path from createData for each MDX.
465
434
  const aliasedPath = aliasedSitePath(mdxPath, siteDir);
466
435
  return path.join(
467
436
  dataDir,
@@ -471,6 +440,21 @@ export default function pluginContentBlog(
471
440
  // For blog posts a title in markdown is always removed
472
441
  // Blog posts title are rendered separately
473
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
+ }),
474
458
  },
475
459
  },
476
460
  {
@@ -484,72 +468,55 @@ export default function pluginContentBlog(
484
468
  };
485
469
  },
486
470
 
487
- async postBuild({outDir}: Props) {
488
- if (!options.feedOptions?.type) {
471
+ async postBuild({outDir, content}) {
472
+ if (!options.feedOptions.type) {
489
473
  return;
490
474
  }
491
-
492
- const feed = await generateBlogFeed(contentPaths, context, options);
493
-
494
- if (!feed) {
475
+ const {blogPosts} = content;
476
+ if (!blogPosts.length) {
495
477
  return;
496
478
  }
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
- );
479
+ await createBlogFeedFiles({
480
+ blogPosts,
481
+ options,
482
+ outDir,
483
+ siteConfig,
484
+ locale: currentLocale,
485
+ });
515
486
  },
516
487
 
517
488
  injectHtmlTags({content}) {
518
- if (!content.blogPosts.length) {
519
- return {};
520
- }
521
-
522
- if (!options.feedOptions?.type) {
489
+ if (!content.blogPosts.length || !options.feedOptions.type) {
523
490
  return {};
524
491
  }
525
492
 
526
493
  const feedTypes = options.feedOptions.type;
527
- const {
528
- siteConfig: {title},
529
- baseUrl,
530
- } = context;
494
+ const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
531
495
  const feedsConfig = {
532
496
  rss: {
533
497
  type: 'application/rss+xml',
534
498
  path: 'rss.xml',
535
- title: `${title} Blog RSS Feed`,
499
+ title: `${feedTitle} RSS Feed`,
536
500
  },
537
501
  atom: {
538
502
  type: 'application/atom+xml',
539
503
  path: 'atom.xml',
540
- 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`,
541
510
  },
542
511
  };
543
512
  const headTags: HtmlTags = [];
544
513
 
545
514
  feedTypes.forEach((feedType) => {
546
- const feedConfig = feedsConfig[feedType] || {};
547
-
548
- if (!feedsConfig) {
549
- return;
550
- }
551
-
552
- const {type, path: feedConfigPath, title: feedConfigTitle} = feedConfig;
515
+ const {
516
+ type,
517
+ path: feedConfigPath,
518
+ title: feedConfigTitle,
519
+ } = feedsConfig[feedType];
553
520
 
554
521
  headTags.push({
555
522
  tagName: 'link',
@@ -573,10 +540,4 @@ export default function pluginContentBlog(
573
540
  };
574
541
  }
575
542
 
576
- export function validateOptions({
577
- validate,
578
- options,
579
- }: OptionValidationContext<PluginOptions>): ValidationResult<PluginOptions> {
580
- const validatedOptions = validate(PluginOptionSchema, options);
581
- return validatedOptions;
582
- }
543
+ export {validateOptions} from './options';