@docusaurus/plugin-content-blog 2.0.0-beta.12faed89d → 2.0.0-beta.14

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 (49) hide show
  1. package/lib/authors.d.ts +23 -0
  2. package/lib/authors.js +147 -0
  3. package/lib/blogFrontMatter.d.ts +19 -6
  4. package/lib/blogFrontMatter.js +31 -19
  5. package/lib/blogUtils.d.ts +10 -4
  6. package/lib/blogUtils.js +142 -137
  7. package/lib/feed.d.ts +20 -0
  8. package/lib/feed.js +105 -0
  9. package/lib/index.js +104 -106
  10. package/lib/markdownLoader.d.ts +3 -6
  11. package/lib/markdownLoader.js +5 -5
  12. package/lib/pluginOptionSchema.d.ts +3 -26
  13. package/lib/pluginOptionSchema.js +30 -9
  14. package/lib/translations.d.ts +10 -0
  15. package/lib/translations.js +53 -0
  16. package/lib/types.d.ts +55 -15
  17. package/package.json +17 -13
  18. package/src/authors.ts +196 -0
  19. package/src/blogFrontMatter.ts +71 -33
  20. package/src/blogUtils.ts +196 -181
  21. package/{types.d.ts → src/deps.d.ts} +0 -0
  22. package/src/feed.ts +149 -0
  23. package/src/index.ts +123 -107
  24. package/src/markdownLoader.ts +8 -12
  25. package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
  26. package/src/pluginOptionSchema.ts +34 -12
  27. package/src/translations.ts +63 -0
  28. package/src/types.ts +69 -16
  29. package/lib/.tsbuildinfo +0 -1
  30. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  31. package/src/__tests__/__fixtures__/website/blog/complex-slug.md +0 -7
  32. package/src/__tests__/__fixtures__/website/blog/date-matter.md +0 -5
  33. package/src/__tests__/__fixtures__/website/blog/draft.md +0 -6
  34. package/src/__tests__/__fixtures__/website/blog/heading-as-title.md +0 -5
  35. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +0 -7
  36. package/src/__tests__/__fixtures__/website/blog-with-ref/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  37. package/src/__tests__/__fixtures__/website/blog-with-ref/post-with-broken-links.md +0 -11
  38. package/src/__tests__/__fixtures__/website/blog-with-ref/post.md +0 -5
  39. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  40. package/src/__tests__/__fixtures__/website-blog-without-date/blog/no date.md +0 -1
  41. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -76
  42. package/src/__tests__/__snapshots__/linkify.test.ts.snap +0 -24
  43. package/src/__tests__/__snapshots__/pluginOptionSchema.test.ts.snap +0 -5
  44. package/src/__tests__/blogFrontMatter.test.ts +0 -317
  45. package/src/__tests__/generateBlogFeed.test.ts +0 -100
  46. package/src/__tests__/index.test.ts +0 -336
  47. package/src/__tests__/linkify.test.ts +0 -93
  48. package/src/__tests__/pluginOptionSchema.test.ts +0 -150
  49. package/tsconfig.json +0 -9
package/src/feed.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import {Feed, Author as FeedAuthor, Item as FeedItem} from 'feed';
9
+ import {PluginOptions, Author, BlogPost, FeedType} from './types';
10
+ import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
11
+ import {DocusaurusConfig} from '@docusaurus/types';
12
+ import path from 'path';
13
+ import fs from 'fs-extra';
14
+
15
+ // TODO this is temporary until we handle mdxToHtml better
16
+ // It's hard to convert reliably JSX/require calls to an html feed content
17
+ // See https://github.com/facebook/docusaurus/issues/5664
18
+ function mdxToFeedContent(mdxContent: string): string | undefined {
19
+ try {
20
+ return mdxToHtml(mdxContent);
21
+ } catch (e) {
22
+ // TODO will we need a plugin option to configure how to handle such an error
23
+ // Swallow the error on purpose for now, until we understand better the problem space
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ export async function generateBlogFeed({
29
+ blogPosts,
30
+ options,
31
+ siteConfig,
32
+ }: {
33
+ blogPosts: BlogPost[];
34
+ options: PluginOptions;
35
+ siteConfig: DocusaurusConfig;
36
+ }): Promise<Feed | null> {
37
+ if (!blogPosts.length) {
38
+ return null;
39
+ }
40
+
41
+ const {feedOptions, routeBasePath} = options;
42
+ const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
43
+ const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
44
+
45
+ const updated =
46
+ (blogPosts[0] && blogPosts[0].metadata.date) ||
47
+ new Date('2015-10-25T16:29:00.000-07:00'); // weird legacy magic date
48
+
49
+ const feed = new Feed({
50
+ id: blogBaseUrl,
51
+ title: feedOptions.title || `${title} Blog`,
52
+ updated,
53
+ language: feedOptions.language,
54
+ link: blogBaseUrl,
55
+ description: feedOptions.description || `${siteConfig.title} Blog`,
56
+ favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
57
+ copyright: feedOptions.copyright,
58
+ });
59
+
60
+ function toFeedAuthor(author: Author): FeedAuthor {
61
+ // TODO ask author emails?
62
+ // RSS feed requires email to render authors
63
+ return {name: author.name, link: author.url};
64
+ }
65
+
66
+ blogPosts.forEach((post) => {
67
+ const {
68
+ id,
69
+ metadata: {title: metadataTitle, permalink, date, description, authors},
70
+ } = post;
71
+
72
+ const feedItem: FeedItem = {
73
+ title: metadataTitle,
74
+ id,
75
+ link: normalizeUrl([siteUrl, permalink]),
76
+ date,
77
+ description,
78
+ content: mdxToFeedContent(post.content),
79
+ };
80
+
81
+ // json1() method takes the first item of authors array
82
+ // it causes an error when authors array is empty
83
+ const feedItemAuthors = authors.map(toFeedAuthor);
84
+ if (feedItemAuthors.length > 0) {
85
+ feedItem.author = feedItemAuthors;
86
+ }
87
+
88
+ feed.addItem(feedItem);
89
+ });
90
+
91
+ return feed;
92
+ }
93
+
94
+ async function createBlogFeedFile({
95
+ feed,
96
+ feedType,
97
+ generatePath,
98
+ }: {
99
+ feed: Feed;
100
+ feedType: FeedType;
101
+ generatePath: string;
102
+ }) {
103
+ const [feedContent, feedPath] = (() => {
104
+ switch (feedType) {
105
+ case 'rss':
106
+ return [feed.rss2(), 'rss.xml'];
107
+ case 'json':
108
+ return [feed.json1(), 'feed.json'];
109
+ case 'atom':
110
+ return [feed.atom1(), 'atom.xml'];
111
+ default:
112
+ throw new Error(`Feed type ${feedType} not supported.`);
113
+ }
114
+ })();
115
+ try {
116
+ await fs.outputFile(path.join(generatePath, feedPath), feedContent);
117
+ } catch (err) {
118
+ throw new Error(`Generating ${feedType} feed failed: ${err}.`);
119
+ }
120
+ }
121
+
122
+ export async function createBlogFeedFiles({
123
+ blogPosts,
124
+ options,
125
+ siteConfig,
126
+ outDir,
127
+ }: {
128
+ blogPosts: BlogPost[];
129
+ options: PluginOptions;
130
+ siteConfig: DocusaurusConfig;
131
+ outDir: string;
132
+ }): Promise<void> {
133
+ const feed = await generateBlogFeed({blogPosts, options, siteConfig});
134
+
135
+ const feedTypes = options.feedOptions.type;
136
+ if (!feed || !feedTypes) {
137
+ return;
138
+ }
139
+
140
+ await Promise.all(
141
+ feedTypes.map((feedType) =>
142
+ createBlogFeedFile({
143
+ feed,
144
+ feedType,
145
+ generatePath: path.join(outDir, options.routeBasePath),
146
+ }),
147
+ ),
148
+ );
149
+ }
package/src/index.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import fs from 'fs-extra';
9
8
  import path from 'path';
10
9
  import admonitions from 'remark-admonitions';
11
10
  import {
@@ -16,12 +15,10 @@ import {
16
15
  reportMessage,
17
16
  posixPath,
18
17
  addTrailingPathSeparator,
19
- } from '@docusaurus/utils';
20
- import {
21
- STATIC_DIR_NAME,
18
+ createAbsoluteFilePathMatcher,
22
19
  DEFAULT_PLUGIN_ID,
23
- } from '@docusaurus/core/lib/constants';
24
- import {flatten, take, kebabCase} from 'lodash';
20
+ } from '@docusaurus/utils';
21
+ import {translateContent, getTranslationFiles} from './translations';
25
22
 
26
23
  import {
27
24
  PluginOptions,
@@ -30,9 +27,10 @@ import {
30
27
  BlogItemsToMetadata,
31
28
  TagsModule,
32
29
  BlogPaginated,
33
- BlogPost,
34
30
  BlogContentPaths,
35
31
  BlogMarkdownLoaderOptions,
32
+ MetaData,
33
+ Assets,
36
34
  } from './types';
37
35
  import {PluginOptionSchema} from './pluginOptionSchema';
38
36
  import {
@@ -46,11 +44,13 @@ import {
46
44
  } from '@docusaurus/types';
47
45
  import {Configuration} from 'webpack';
48
46
  import {
49
- generateBlogFeed,
50
47
  generateBlogPosts,
51
48
  getContentPathList,
52
49
  getSourceToPermalink,
50
+ getBlogTags,
53
51
  } from './blogUtils';
52
+ import {BlogPostFrontMatter} from './blogFrontMatter';
53
+ import {createBlogFeedFiles} from './feed';
54
54
 
55
55
  export default function pluginContentBlog(
56
56
  context: LoadContext,
@@ -64,10 +64,11 @@ export default function pluginContentBlog(
64
64
 
65
65
  const {
66
66
  siteDir,
67
- siteConfig: {onBrokenMarkdownLinks},
67
+ siteConfig,
68
68
  generatedFilesDir,
69
69
  i18n: {currentLocale},
70
70
  } = context;
71
+ const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
71
72
 
72
73
  const contentPaths: BlogContentPaths = {
73
74
  contentPath: path.resolve(siteDir, options.path),
@@ -92,36 +93,42 @@ export default function pluginContentBlog(
92
93
  name: 'docusaurus-plugin-content-blog',
93
94
 
94
95
  getPathsToWatch() {
95
- const {include = []} = options;
96
- return flatten(
97
- getContentPathList(contentPaths).map((contentPath) => {
98
- return include.map((pattern) => `${contentPath}/${pattern}`);
99
- }),
96
+ const {include, authorsMapPath} = options;
97
+ const contentMarkdownGlobs = getContentPathList(contentPaths).flatMap(
98
+ (contentPath) => include.map((pattern) => `${contentPath}/${pattern}`),
100
99
  );
101
- },
102
100
 
103
- getClientModules() {
104
- const modules = [];
101
+ // TODO: we should read this path in plugin! but plugins do not support async init for now :'(
102
+ // const authorsMapFilePath = await getAuthorsMapFilePath({authorsMapPath,contentPaths,});
103
+ // simplified impl, better than nothing for now:
104
+ const authorsMapFilePath = path.join(
105
+ contentPaths.contentPath,
106
+ authorsMapPath,
107
+ );
105
108
 
106
- if (options.admonitions) {
107
- modules.push(require.resolve('remark-admonitions/styles/infima.css'));
108
- }
109
+ return [authorsMapFilePath, ...contentMarkdownGlobs];
110
+ },
109
111
 
110
- return modules;
112
+ async getTranslationFiles() {
113
+ return getTranslationFiles(options);
111
114
  },
112
115
 
113
116
  // Fetches blog contents and returns metadata for the necessary routes.
114
117
  async loadContent() {
115
- const {postsPerPage, routeBasePath} = options;
118
+ const {
119
+ postsPerPage: postsPerPageOption,
120
+ routeBasePath,
121
+ tagsBasePath,
122
+ blogDescription,
123
+ blogTitle,
124
+ blogSidebarTitle,
125
+ } = options;
116
126
 
117
- const blogPosts: BlogPost[] = await generateBlogPosts(
118
- contentPaths,
119
- context,
120
- options,
121
- );
127
+ const blogPosts = await generateBlogPosts(contentPaths, context, options);
122
128
 
123
129
  if (!blogPosts.length) {
124
130
  return {
131
+ blogSidebarTitle,
125
132
  blogPosts: [],
126
133
  blogListPaginated: [],
127
134
  blogTags: {},
@@ -152,18 +159,17 @@ export default function pluginContentBlog(
152
159
  // Blog pagination routes.
153
160
  // Example: `/blog`, `/blog/page/1`, `/blog/page/2`
154
161
  const totalCount = blogPosts.length;
162
+ const postsPerPage =
163
+ postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
155
164
  const numberOfPages = Math.ceil(totalCount / postsPerPage);
156
- const {
157
- siteConfig: {baseUrl = ''},
158
- } = context;
159
- const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
165
+ const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
160
166
 
161
167
  const blogListPaginated: BlogPaginated[] = [];
162
168
 
163
169
  function blogPaginationPermalink(page: number) {
164
170
  return page > 0
165
- ? normalizeUrl([basePageUrl, `page/${page + 1}`])
166
- : basePageUrl;
171
+ ? normalizeUrl([baseBlogUrl, `page/${page + 1}`])
172
+ : baseBlogUrl;
167
173
  }
168
174
 
169
175
  for (let page = 0; page < numberOfPages; page += 1) {
@@ -179,8 +185,8 @@ export default function pluginContentBlog(
179
185
  page < numberOfPages - 1
180
186
  ? blogPaginationPermalink(page + 1)
181
187
  : null,
182
- blogDescription: options.blogDescription,
183
- blogTitle: options.blogTitle,
188
+ blogDescription,
189
+ blogTitle,
184
190
  },
185
191
  items: blogPosts
186
192
  .slice(page * postsPerPage, (page + 1) * postsPerPage)
@@ -188,46 +194,15 @@ export default function pluginContentBlog(
188
194
  });
189
195
  }
190
196
 
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
- }
197
+ const blogTags: BlogTags = getBlogTags(blogPosts);
201
198
 
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
- });
225
- });
199
+ const tagsPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
226
200
 
227
201
  const blogTagsListPath =
228
202
  Object.keys(blogTags).length > 0 ? tagsPath : null;
229
203
 
230
204
  return {
205
+ blogSidebarTitle,
231
206
  blogPosts,
232
207
  blogListPaginated,
233
208
  blogTags,
@@ -245,10 +220,13 @@ export default function pluginContentBlog(
245
220
  blogPostComponent,
246
221
  blogTagsListComponent,
247
222
  blogTagsPostsComponent,
223
+ routeBasePath,
224
+ archiveBasePath,
248
225
  } = options;
249
226
 
250
227
  const {addRoute, createData} = actions;
251
228
  const {
229
+ blogSidebarTitle,
252
230
  blogPosts,
253
231
  blogListPaginated,
254
232
  blogTags,
@@ -260,7 +238,27 @@ export default function pluginContentBlog(
260
238
  const sidebarBlogPosts =
261
239
  options.blogSidebarCount === 'ALL'
262
240
  ? blogPosts
263
- : take(blogPosts, options.blogSidebarCount);
241
+ : blogPosts.slice(0, options.blogSidebarCount);
242
+
243
+ const archiveUrl = normalizeUrl([
244
+ baseUrl,
245
+ routeBasePath,
246
+ archiveBasePath,
247
+ ]);
248
+
249
+ // creates a blog archive route
250
+ const archiveProp = await createData(
251
+ `${docuHash(archiveUrl)}.json`,
252
+ JSON.stringify({blogPosts}, null, 2),
253
+ );
254
+ addRoute({
255
+ path: archiveUrl,
256
+ component: '@theme/BlogArchivePage',
257
+ exact: true,
258
+ modules: {
259
+ archive: aliasedSource(archiveProp),
260
+ },
261
+ });
264
262
 
265
263
  // This prop is useful to provide the blog list sidebar
266
264
  const sidebarProp = await createData(
@@ -269,7 +267,7 @@ export default function pluginContentBlog(
269
267
  `blog-post-list-prop-${pluginId}.json`,
270
268
  JSON.stringify(
271
269
  {
272
- title: options.blogSidebarTitle,
270
+ title: blogSidebarTitle,
273
271
  items: sidebarBlogPosts.map((blogPost) => ({
274
272
  title: blogPost.metadata.title,
275
273
  permalink: blogPost.metadata.permalink,
@@ -321,9 +319,9 @@ export default function pluginContentBlog(
321
319
  exact: true,
322
320
  modules: {
323
321
  sidebar: aliasedSource(sidebarProp),
324
- items: items.map((postID) => {
322
+ items: items.map((postID) =>
325
323
  // To tell routes.js this is an import and not a nested object to recurse.
326
- return {
324
+ ({
327
325
  content: {
328
326
  __import: true,
329
327
  path: blogItemsToMetadata[postID].source,
@@ -331,8 +329,8 @@ export default function pluginContentBlog(
331
329
  truncated: true,
332
330
  },
333
331
  },
334
- };
335
- }),
332
+ }),
333
+ ),
336
334
  metadata: aliasedSource(pageMetadataPath),
337
335
  },
338
336
  });
@@ -350,6 +348,7 @@ export default function pluginContentBlog(
350
348
  Object.keys(blogTags).map(async (tag) => {
351
349
  const {name, items, permalink} = blogTags[tag];
352
350
 
351
+ // Refactor all this, see docs implementation
353
352
  tagsModule[tag] = {
354
353
  allTagsPath: blogTagsListPath,
355
354
  slug: tag,
@@ -406,6 +405,10 @@ export default function pluginContentBlog(
406
405
  }
407
406
  },
408
407
 
408
+ translateContent({content, translationFiles}) {
409
+ return translateContent(content, translationFiles);
410
+ },
411
+
409
412
  configureWebpack(
410
413
  _config: Configuration,
411
414
  isServer: boolean,
@@ -436,6 +439,7 @@ export default function pluginContentBlog(
436
439
  },
437
440
  };
438
441
 
442
+ const contentDirs = getContentPathList(contentPaths);
439
443
  return {
440
444
  resolve: {
441
445
  alias: {
@@ -446,7 +450,7 @@ export default function pluginContentBlog(
446
450
  rules: [
447
451
  {
448
452
  test: /(\.mdx?)$/,
449
- include: getContentPathList(contentPaths)
453
+ include: contentDirs
450
454
  // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
451
455
  .map(addTrailingPathSeparator),
452
456
  use: [
@@ -458,10 +462,17 @@ export default function pluginContentBlog(
458
462
  rehypePlugins,
459
463
  beforeDefaultRemarkPlugins,
460
464
  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.
465
+ staticDirs: siteConfig.staticDirectories.map((dir) =>
466
+ path.resolve(siteDir, dir),
467
+ ),
468
+ siteDir,
469
+ isMDXPartial: createAbsoluteFilePathMatcher(
470
+ options.exclude,
471
+ contentDirs,
472
+ ),
464
473
  metadataPath: (mdxPath: string) => {
474
+ // Note that metadataPath must be the same/in-sync as
475
+ // the path from createData for each MDX.
465
476
  const aliasedPath = aliasedSitePath(mdxPath, siteDir);
466
477
  return path.join(
467
478
  dataDir,
@@ -471,6 +482,20 @@ export default function pluginContentBlog(
471
482
  // For blog posts a title in markdown is always removed
472
483
  // Blog posts title are rendered separately
473
484
  removeContentTitle: true,
485
+
486
+ // Assets allow to convert some relative images paths to require() calls
487
+ createAssets: ({
488
+ frontMatter,
489
+ metadata,
490
+ }: {
491
+ frontMatter: BlogPostFrontMatter;
492
+ metadata: MetaData;
493
+ }): Assets => ({
494
+ image: frontMatter.image,
495
+ authorsImageUrls: metadata.authors.map(
496
+ (author) => author.imageURL,
497
+ ),
498
+ }),
474
499
  },
475
500
  },
476
501
  {
@@ -485,33 +510,22 @@ export default function pluginContentBlog(
485
510
  },
486
511
 
487
512
  async postBuild({outDir}: Props) {
488
- if (!options.feedOptions?.type) {
513
+ if (!options.feedOptions.type) {
489
514
  return;
490
515
  }
491
516
 
492
- const feed = await generateBlogFeed(contentPaths, context, options);
493
-
494
- if (!feed) {
517
+ // TODO: we shouldn't need to re-read the posts here!
518
+ // postBuild should receive loadedContent
519
+ const blogPosts = await generateBlogPosts(contentPaths, context, options);
520
+ if (!blogPosts.length) {
495
521
  return;
496
522
  }
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
- );
523
+ await createBlogFeedFiles({
524
+ blogPosts,
525
+ options,
526
+ outDir,
527
+ siteConfig,
528
+ });
515
529
  },
516
530
 
517
531
  injectHtmlTags({content}) {
@@ -524,20 +538,22 @@ export default function pluginContentBlog(
524
538
  }
525
539
 
526
540
  const feedTypes = options.feedOptions.type;
527
- const {
528
- siteConfig: {title},
529
- baseUrl,
530
- } = context;
541
+ const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
531
542
  const feedsConfig = {
532
543
  rss: {
533
544
  type: 'application/rss+xml',
534
545
  path: 'rss.xml',
535
- title: `${title} Blog RSS Feed`,
546
+ title: `${feedTitle} RSS Feed`,
536
547
  },
537
548
  atom: {
538
549
  type: 'application/atom+xml',
539
550
  path: 'atom.xml',
540
- title: `${title} Blog Atom Feed`,
551
+ title: `${feedTitle} Atom Feed`,
552
+ },
553
+ json: {
554
+ type: 'application/json',
555
+ path: 'feed.json',
556
+ title: `${feedTitle} JSON Feed`,
541
557
  },
542
558
  };
543
559
  const headTags: HtmlTags = [];
@@ -8,18 +8,16 @@
8
8
  import {truncate, linkify} from './blogUtils';
9
9
  import {parseQuery} from 'loader-utils';
10
10
  import {BlogMarkdownLoaderOptions} from './types';
11
+ import type {LoaderContext} from 'webpack';
11
12
 
12
- // TODO temporary until Webpack5 export this type
13
- // see https://github.com/webpack/webpack/issues/11630
14
- interface Loader extends Function {
15
- (this: any, source: string): string | Buffer | void | undefined;
16
- }
17
-
18
- const markdownLoader: Loader = function (source) {
13
+ export default function markdownLoader(
14
+ this: LoaderContext<BlogMarkdownLoaderOptions>,
15
+ source: string,
16
+ ): void {
19
17
  const filePath = this.resourcePath;
20
- const fileString = source as string;
18
+ const fileString = source;
21
19
  const callback = this.async();
22
- const markdownLoaderOptions = this.getOptions() as BlogMarkdownLoaderOptions;
20
+ const markdownLoaderOptions = this.getOptions();
23
21
 
24
22
  // Linkify blog posts
25
23
  let finalContent = linkify({
@@ -38,6 +36,4 @@ const markdownLoader: Loader = function (source) {
38
36
  }
39
37
 
40
38
  return callback && callback(null, finalContent);
41
- };
42
-
43
- export default markdownLoader;
39
+ }