@docusaurus/plugin-content-blog 2.0.0-beta.1ec2c95e3 → 2.0.0-beta.21

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