@docusaurus/plugin-content-blog 3.3.2 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/assets/atom.css +75 -0
  2. package/assets/atom.xsl +92 -0
  3. package/assets/rss.css +75 -0
  4. package/assets/rss.xsl +86 -0
  5. package/lib/authors.d.ts +9 -11
  6. package/lib/authors.js +42 -64
  7. package/lib/authorsMap.d.ts +23 -0
  8. package/lib/authorsMap.js +116 -0
  9. package/lib/authorsProblems.d.ts +21 -0
  10. package/lib/authorsProblems.js +51 -0
  11. package/lib/authorsSocials.d.ts +10 -0
  12. package/lib/authorsSocials.js +48 -0
  13. package/lib/blogUtils.d.ts +7 -12
  14. package/lib/blogUtils.js +44 -34
  15. package/lib/client/contexts.d.ts +33 -0
  16. package/lib/client/contexts.js +54 -0
  17. package/lib/client/index.d.ts +3 -3
  18. package/lib/client/index.js +3 -9
  19. package/lib/client/sidebarUtils.d.ts +21 -0
  20. package/lib/client/sidebarUtils.js +49 -0
  21. package/lib/client/sidebarUtils.test.d.ts +7 -0
  22. package/lib/client/sidebarUtils.test.js +43 -0
  23. package/lib/client/structuredDataUtils.d.ts +10 -0
  24. package/lib/client/structuredDataUtils.js +122 -0
  25. package/lib/feed.d.ts +8 -3
  26. package/lib/feed.js +111 -20
  27. package/lib/frontMatter.d.ts +0 -1
  28. package/lib/frontMatter.js +3 -2
  29. package/lib/index.d.ts +0 -1
  30. package/lib/index.js +132 -105
  31. package/lib/markdownLoader.js +3 -7
  32. package/lib/options.d.ts +4 -1
  33. package/lib/options.js +107 -26
  34. package/lib/props.d.ts +9 -2
  35. package/lib/props.js +23 -3
  36. package/lib/remark/footnoteIDFixer.js +1 -1
  37. package/lib/routes.d.ts +0 -1
  38. package/lib/routes.js +82 -14
  39. package/lib/translations.d.ts +0 -1
  40. package/lib/translations.js +2 -3
  41. package/lib/types.d.ts +1 -8
  42. package/package.json +13 -10
  43. package/src/authors.ts +56 -93
  44. package/src/authorsMap.ts +171 -0
  45. package/src/authorsProblems.ts +72 -0
  46. package/src/authorsSocials.ts +64 -0
  47. package/src/blogUtils.ts +51 -46
  48. package/src/client/contexts.tsx +95 -0
  49. package/src/client/index.tsx +24 -0
  50. package/src/client/sidebarUtils.test.ts +52 -0
  51. package/src/client/sidebarUtils.tsx +85 -0
  52. package/src/client/structuredDataUtils.ts +178 -0
  53. package/src/feed.ts +197 -18
  54. package/src/frontMatter.ts +2 -0
  55. package/src/index.ts +182 -137
  56. package/src/markdownLoader.ts +3 -7
  57. package/src/options.ts +132 -32
  58. package/src/plugin-content-blog.d.ts +252 -113
  59. package/src/props.ts +41 -1
  60. package/src/routes.ts +102 -12
  61. package/src/types.ts +1 -6
  62. package/src/client/index.ts +0 -20
package/src/index.ts CHANGED
@@ -18,22 +18,26 @@ import {
18
18
  getContentPathList,
19
19
  getDataFilePath,
20
20
  DEFAULT_PLUGIN_ID,
21
+ resolveMarkdownLinkPathname,
22
+ type SourceToPermalink,
21
23
  } from '@docusaurus/utils';
24
+ import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation';
22
25
  import {
23
- getSourceToPermalink,
24
26
  getBlogTags,
25
27
  paginateBlogPosts,
26
28
  shouldBeListed,
27
29
  applyProcessBlogPosts,
28
30
  generateBlogPosts,
31
+ reportUntruncatedBlogPosts,
29
32
  } from './blogUtils';
30
33
  import footnoteIDFixer from './remark/footnoteIDFixer';
31
34
  import {translateContent, getTranslationFiles} from './translations';
32
- import {createBlogFeedFiles} from './feed';
35
+ import {createBlogFeedFiles, createFeedHtmlHeadTags} from './feed';
33
36
 
34
37
  import {createAllRoutes} from './routes';
38
+ import {checkAuthorsMapPermalinkCollisions, getAuthorsMap} from './authorsMap';
35
39
  import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
36
- import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
40
+ import type {LoadContext, Plugin} from '@docusaurus/types';
37
41
  import type {
38
42
  PluginOptions,
39
43
  BlogPostFrontMatter,
@@ -43,6 +47,37 @@ import type {
43
47
  BlogContent,
44
48
  BlogPaginated,
45
49
  } from '@docusaurus/plugin-content-blog';
50
+ import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader/lib/loader';
51
+ import type {RuleSetUseItem} from 'webpack';
52
+
53
+ const PluginName = 'docusaurus-plugin-content-blog';
54
+
55
+ // TODO this is bad, we should have a better way to do this (new lifecycle?)
56
+ // The source to permalink is currently a mutable map passed to the mdx loader
57
+ // for link resolution
58
+ // see https://github.com/facebook/docusaurus/pull/10185
59
+ function createSourceToPermalinkHelper() {
60
+ const sourceToPermalink: SourceToPermalink = new Map();
61
+
62
+ function computeSourceToPermalink(content: BlogContent): SourceToPermalink {
63
+ return new Map(
64
+ content.blogPosts.map(({metadata: {source, permalink}}) => [
65
+ source,
66
+ permalink,
67
+ ]),
68
+ );
69
+ }
70
+
71
+ // Mutable map update :/
72
+ function update(content: BlogContent): void {
73
+ sourceToPermalink.clear();
74
+ computeSourceToPermalink(content).forEach((value, key) => {
75
+ sourceToPermalink.set(key, value);
76
+ });
77
+ }
78
+
79
+ return {get: () => sourceToPermalink, update};
80
+ }
46
81
 
47
82
  export default async function pluginContentBlog(
48
83
  context: LoadContext,
@@ -55,22 +90,29 @@ export default async function pluginContentBlog(
55
90
  localizationDir,
56
91
  i18n: {currentLocale},
57
92
  } = context;
93
+
94
+ const router = siteConfig.future.experimental_router;
95
+ const isBlogFeedDisabledBecauseOfHashRouter =
96
+ router === 'hash' && !!options.feedOptions.type;
97
+ if (isBlogFeedDisabledBecauseOfHashRouter) {
98
+ logger.warn(
99
+ `${PluginName} feed feature does not support the Hash Router. Feeds won't be generated.`,
100
+ );
101
+ }
102
+
58
103
  const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
59
104
 
60
105
  const contentPaths: BlogContentPaths = {
61
106
  contentPath: path.resolve(siteDir, options.path),
62
107
  contentPathLocalized: getPluginI18nPath({
63
108
  localizationDir,
64
- pluginName: 'docusaurus-plugin-content-blog',
109
+ pluginName: PluginName,
65
110
  pluginId: options.id,
66
111
  }),
67
112
  };
68
113
  const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
69
114
 
70
- const pluginDataDirRoot = path.join(
71
- generatedFilesDir,
72
- 'docusaurus-plugin-content-blog',
73
- );
115
+ const pluginDataDirRoot = path.join(generatedFilesDir, PluginName);
74
116
  const dataDir = path.join(pluginDataDirRoot, pluginId);
75
117
  // TODO Docusaurus v4 breaking change
76
118
  // module aliasing should be automatic
@@ -83,8 +125,10 @@ export default async function pluginContentBlog(
83
125
  contentPaths,
84
126
  });
85
127
 
128
+ const sourceToPermalinkHelper = createSourceToPermalinkHelper();
129
+
86
130
  return {
87
- name: 'docusaurus-plugin-content-blog',
131
+ name: PluginName,
88
132
 
89
133
  getPathsToWatch() {
90
134
  const {include} = options;
@@ -92,9 +136,16 @@ export default async function pluginContentBlog(
92
136
  (contentPath) => include.map((pattern) => `${contentPath}/${pattern}`),
93
137
  );
94
138
 
95
- return [authorsMapFilePath, ...contentMarkdownGlobs].filter(
96
- Boolean,
97
- ) as string[];
139
+ const tagsFilePaths = getTagsFilePathsToWatch({
140
+ contentPaths,
141
+ tags: options.tags,
142
+ });
143
+
144
+ return [
145
+ authorsMapFilePath,
146
+ ...tagsFilePaths,
147
+ ...contentMarkdownGlobs,
148
+ ].filter(Boolean) as string[];
98
149
  },
99
150
 
100
151
  getTranslationFiles() {
@@ -111,15 +162,38 @@ export default async function pluginContentBlog(
111
162
  blogTitle,
112
163
  blogSidebarTitle,
113
164
  pageBasePath,
165
+ authorsBasePath,
166
+ authorsMapPath,
114
167
  } = options;
115
168
 
116
169
  const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
117
170
  const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
118
- let blogPosts = await generateBlogPosts(contentPaths, context, options);
171
+
172
+ const authorsMap = await getAuthorsMap({
173
+ contentPaths,
174
+ authorsMapPath,
175
+ authorsBaseRoutePath: normalizeUrl([
176
+ baseUrl,
177
+ routeBasePath,
178
+ authorsBasePath,
179
+ ]),
180
+ });
181
+ checkAuthorsMapPermalinkCollisions(authorsMap);
182
+
183
+ let blogPosts = await generateBlogPosts(
184
+ contentPaths,
185
+ context,
186
+ options,
187
+ authorsMap,
188
+ );
119
189
  blogPosts = await applyProcessBlogPosts({
120
190
  blogPosts,
121
191
  processBlogPosts: options.processBlogPosts,
122
192
  });
193
+ reportUntruncatedBlogPosts({
194
+ blogPosts,
195
+ onUntruncatedBlogPosts: options.onUntruncatedBlogPosts,
196
+ });
123
197
  const listedBlogPosts = blogPosts.filter(shouldBeListed);
124
198
 
125
199
  if (!blogPosts.length) {
@@ -129,6 +203,7 @@ export default async function pluginContentBlog(
129
203
  blogListPaginated: [],
130
204
  blogTags: {},
131
205
  blogTagsListPath,
206
+ authorsMap,
132
207
  };
133
208
  }
134
209
 
@@ -177,10 +252,13 @@ export default async function pluginContentBlog(
177
252
  blogListPaginated,
178
253
  blogTags,
179
254
  blogTagsListPath,
255
+ authorsMap,
180
256
  };
181
257
  },
182
258
 
183
259
  async contentLoaded({content, actions}) {
260
+ sourceToPermalinkHelper.update(content);
261
+
184
262
  await createAllRoutes({
185
263
  baseUrl,
186
264
  content,
@@ -194,32 +272,92 @@ export default async function pluginContentBlog(
194
272
  return translateContent(content, translationFiles);
195
273
  },
196
274
 
197
- configureWebpack(_config, isServer, utils, content) {
275
+ configureWebpack() {
198
276
  const {
199
277
  admonitions,
200
278
  rehypePlugins,
201
279
  remarkPlugins,
280
+ recmaPlugins,
202
281
  truncateMarker,
203
282
  beforeDefaultRemarkPlugins,
204
283
  beforeDefaultRehypePlugins,
205
284
  } = options;
206
285
 
207
- const markdownLoaderOptions: BlogMarkdownLoaderOptions = {
208
- siteDir,
209
- contentPaths,
210
- truncateMarker,
211
- sourceToPermalink: getSourceToPermalink(content.blogPosts),
212
- onBrokenMarkdownLink: (brokenMarkdownLink) => {
213
- if (onBrokenMarkdownLinks === 'ignore') {
214
- return;
215
- }
216
- logger.report(
217
- onBrokenMarkdownLinks,
218
- )`Blog markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath}`;
219
- },
220
- };
221
-
222
286
  const contentDirs = getContentPathList(contentPaths);
287
+
288
+ function createMDXLoader(): RuleSetUseItem {
289
+ const loaderOptions: MDXLoaderOptions = {
290
+ admonitions,
291
+ remarkPlugins,
292
+ rehypePlugins,
293
+ recmaPlugins,
294
+ beforeDefaultRemarkPlugins: [
295
+ footnoteIDFixer,
296
+ ...beforeDefaultRemarkPlugins,
297
+ ],
298
+ beforeDefaultRehypePlugins,
299
+ staticDirs: siteConfig.staticDirectories.map((dir) =>
300
+ path.resolve(siteDir, dir),
301
+ ),
302
+ siteDir,
303
+ isMDXPartial: createAbsoluteFilePathMatcher(
304
+ options.exclude,
305
+ contentDirs,
306
+ ),
307
+ metadataPath: (mdxPath: string) => {
308
+ // Note that metadataPath must be the same/in-sync as
309
+ // the path from createData for each MDX.
310
+ const aliasedPath = aliasedSitePath(mdxPath, siteDir);
311
+ return path.join(dataDir, `${docuHash(aliasedPath)}.json`);
312
+ },
313
+ // For blog posts a title in markdown is always removed
314
+ // Blog posts title are rendered separately
315
+ removeContentTitle: true,
316
+ // Assets allow to convert some relative images paths to
317
+ // require() calls
318
+ // @ts-expect-error: TODO fix typing issue
319
+ createAssets: ({
320
+ frontMatter,
321
+ metadata,
322
+ }: {
323
+ frontMatter: BlogPostFrontMatter;
324
+ metadata: BlogPostMetadata;
325
+ }): Assets => ({
326
+ image: frontMatter.image,
327
+ authorsImageUrls: metadata.authors.map((author) => author.imageURL),
328
+ }),
329
+ markdownConfig: siteConfig.markdown,
330
+ resolveMarkdownLink: ({linkPathname, sourceFilePath}) => {
331
+ const permalink = resolveMarkdownLinkPathname(linkPathname, {
332
+ sourceFilePath,
333
+ sourceToPermalink: sourceToPermalinkHelper.get(),
334
+ siteDir,
335
+ contentPaths,
336
+ });
337
+ if (permalink === null) {
338
+ logger.report(
339
+ onBrokenMarkdownLinks,
340
+ )`Blog markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath}`;
341
+ }
342
+ return permalink;
343
+ },
344
+ };
345
+ return {
346
+ loader: require.resolve('@docusaurus/mdx-loader'),
347
+ options: loaderOptions,
348
+ };
349
+ }
350
+
351
+ function createBlogMarkdownLoader(): RuleSetUseItem {
352
+ const loaderOptions: BlogMarkdownLoaderOptions = {
353
+ truncateMarker,
354
+ };
355
+ return {
356
+ loader: path.resolve(__dirname, './markdownLoader.js'),
357
+ options: loaderOptions,
358
+ };
359
+ }
360
+
223
361
  return {
224
362
  resolve: {
225
363
  alias: {
@@ -233,61 +371,7 @@ export default async function pluginContentBlog(
233
371
  include: contentDirs
234
372
  // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
235
373
  .map(addTrailingPathSeparator),
236
- use: [
237
- {
238
- loader: require.resolve('@docusaurus/mdx-loader'),
239
- options: {
240
- admonitions,
241
- remarkPlugins,
242
- rehypePlugins,
243
- beforeDefaultRemarkPlugins: [
244
- footnoteIDFixer,
245
- ...beforeDefaultRemarkPlugins,
246
- ],
247
- beforeDefaultRehypePlugins,
248
- staticDirs: siteConfig.staticDirectories.map((dir) =>
249
- path.resolve(siteDir, dir),
250
- ),
251
- siteDir,
252
- isMDXPartial: createAbsoluteFilePathMatcher(
253
- options.exclude,
254
- contentDirs,
255
- ),
256
- metadataPath: (mdxPath: string) => {
257
- // Note that metadataPath must be the same/in-sync as
258
- // the path from createData for each MDX.
259
- const aliasedPath = aliasedSitePath(mdxPath, siteDir);
260
- return path.join(
261
- dataDir,
262
- `${docuHash(aliasedPath)}.json`,
263
- );
264
- },
265
- // For blog posts a title in markdown is always removed
266
- // Blog posts title are rendered separately
267
- removeContentTitle: true,
268
-
269
- // Assets allow to convert some relative images paths to
270
- // require() calls
271
- createAssets: ({
272
- frontMatter,
273
- metadata,
274
- }: {
275
- frontMatter: BlogPostFrontMatter;
276
- metadata: BlogPostMetadata;
277
- }): Assets => ({
278
- image: frontMatter.image,
279
- authorsImageUrls: metadata.authors.map(
280
- (author) => author.imageURL,
281
- ),
282
- }),
283
- markdownConfig: siteConfig.markdown,
284
- },
285
- },
286
- {
287
- loader: path.resolve(__dirname, './markdownLoader.js'),
288
- options: markdownLoaderOptions,
289
- },
290
- ].filter(Boolean),
374
+ use: [createMDXLoader(), createBlogMarkdownLoader()],
291
375
  },
292
376
  ],
293
377
  },
@@ -295,73 +379,34 @@ export default async function pluginContentBlog(
295
379
  },
296
380
 
297
381
  async postBuild({outDir, content}) {
298
- if (!options.feedOptions.type) {
299
- return;
300
- }
301
- const {blogPosts} = content;
302
- if (!blogPosts.length) {
382
+ if (
383
+ !content.blogPosts.length ||
384
+ !options.feedOptions.type ||
385
+ isBlogFeedDisabledBecauseOfHashRouter
386
+ ) {
303
387
  return;
304
388
  }
389
+
305
390
  await createBlogFeedFiles({
306
- blogPosts,
391
+ blogPosts: content.blogPosts,
307
392
  options,
308
393
  outDir,
309
394
  siteConfig,
310
395
  locale: currentLocale,
396
+ contentPaths,
311
397
  });
312
398
  },
313
399
 
314
400
  injectHtmlTags({content}) {
315
- if (!content.blogPosts.length || !options.feedOptions.type) {
401
+ if (
402
+ !content.blogPosts.length ||
403
+ !options.feedOptions.type ||
404
+ isBlogFeedDisabledBecauseOfHashRouter
405
+ ) {
316
406
  return {};
317
407
  }
318
408
 
319
- const feedTypes = options.feedOptions.type;
320
- const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
321
- const feedsConfig = {
322
- rss: {
323
- type: 'application/rss+xml',
324
- path: 'rss.xml',
325
- title: `${feedTitle} RSS Feed`,
326
- },
327
- atom: {
328
- type: 'application/atom+xml',
329
- path: 'atom.xml',
330
- title: `${feedTitle} Atom Feed`,
331
- },
332
- json: {
333
- type: 'application/json',
334
- path: 'feed.json',
335
- title: `${feedTitle} JSON Feed`,
336
- },
337
- };
338
- const headTags: HtmlTags = [];
339
-
340
- feedTypes.forEach((feedType) => {
341
- const {
342
- type,
343
- path: feedConfigPath,
344
- title: feedConfigTitle,
345
- } = feedsConfig[feedType];
346
-
347
- headTags.push({
348
- tagName: 'link',
349
- attributes: {
350
- rel: 'alternate',
351
- type,
352
- href: normalizeUrl([
353
- baseUrl,
354
- options.routeBasePath,
355
- feedConfigPath,
356
- ]),
357
- title: feedConfigTitle,
358
- },
359
- });
360
- });
361
-
362
- return {
363
- headTags,
364
- };
409
+ return {headTags: createFeedHtmlHeadTags({context, options})};
365
410
  },
366
411
  };
367
412
  }
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import {truncate, linkify} from './blogUtils';
8
+ import {truncate} from './blogUtils';
9
9
  import type {BlogMarkdownLoaderOptions} from './types';
10
10
  import type {LoaderContext} from 'webpack';
11
11
 
@@ -13,23 +13,19 @@ export default function markdownLoader(
13
13
  this: LoaderContext<BlogMarkdownLoaderOptions>,
14
14
  source: string,
15
15
  ): void {
16
- const filePath = this.resourcePath;
17
16
  const fileString = source;
18
17
  const callback = this.async();
19
18
  const markdownLoaderOptions = this.getOptions();
20
19
 
21
20
  // Linkify blog posts
22
- let finalContent = linkify({
23
- fileString,
24
- filePath,
25
- ...markdownLoaderOptions,
26
- });
21
+ let finalContent = fileString;
27
22
 
28
23
  // Truncate content if requested (e.g: file.md?truncated=true).
29
24
  const truncated: boolean | undefined = this.resourceQuery
30
25
  ? !!new URLSearchParams(this.resourceQuery.slice(1)).get('truncated')
31
26
  : undefined;
32
27
 
28
+ // TODO truncate with the AST instead of the string ?
33
29
  if (truncated) {
34
30
  finalContent = truncate(finalContent, markdownLoaderOptions.truncateMarker);
35
31
  }