@docusaurus/plugin-content-blog 3.4.0 → 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 (59) 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 +6 -3
  14. package/lib/blogUtils.js +29 -14
  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 +3 -2
  26. package/lib/feed.js +69 -21
  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 +23 -4
  31. package/lib/markdownLoader.js +1 -1
  32. package/lib/options.d.ts +4 -1
  33. package/lib/options.js +98 -26
  34. package/lib/props.d.ts +9 -2
  35. package/lib/props.js +21 -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/package.json +13 -10
  42. package/src/authors.ts +56 -93
  43. package/src/authorsMap.ts +171 -0
  44. package/src/authorsProblems.ts +72 -0
  45. package/src/authorsSocials.ts +64 -0
  46. package/src/blogUtils.ts +34 -7
  47. package/src/client/contexts.tsx +95 -0
  48. package/src/client/index.tsx +24 -0
  49. package/src/client/sidebarUtils.test.ts +52 -0
  50. package/src/client/sidebarUtils.tsx +85 -0
  51. package/src/client/structuredDataUtils.ts +178 -0
  52. package/src/feed.ts +140 -17
  53. package/src/frontMatter.ts +2 -0
  54. package/src/index.ts +31 -1
  55. package/src/options.ts +123 -32
  56. package/src/plugin-content-blog.d.ts +150 -12
  57. package/src/props.ts +39 -1
  58. package/src/routes.ts +102 -12
  59. package/src/client/index.ts +0 -20
@@ -22,13 +22,7 @@ declare module '@docusaurus/plugin-content-blog' {
22
22
 
23
23
  export type Assets = {
24
24
  /**
25
- * If `metadata.yarn workspace website typecheck
26
- 4
27
- yarn workspace v1.22.19yarn workspace website typecheck
28
- 4
29
- yarn workspace v1.22.19yarn workspace website typecheck
30
- 4
31
- yarn workspace v1.22.19image` is a collocated image path, this entry will be the
25
+ * If `metadata.image` is a collocated image path, this entry will be the
32
26
  * bundler-generated image path. Otherwise, it's empty, and the image URL
33
27
  * should be accessed through `frontMatter.image`.
34
28
  */
@@ -43,7 +37,30 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
43
37
  authorsImageUrls: (string | undefined)[];
44
38
  };
45
39
 
46
- export type Author = {
40
+ /**
41
+ * Note we don't pre-define all possible platforms
42
+ * Users can add their own custom platforms if needed
43
+ */
44
+ export type SocialPlatformKey =
45
+ | 'twitter'
46
+ | 'github'
47
+ | 'linkedin'
48
+ | 'stackoverflow'
49
+ | 'x';
50
+
51
+ /**
52
+ * Social platforms of the author.
53
+ * The record value is usually the fully qualified link of the social profile.
54
+ * For pre-defined platforms, it's possible to pass a handle instead
55
+ */
56
+ export type AuthorSocials = Partial<Record<SocialPlatformKey, string>> & {
57
+ /**
58
+ * Unknown keys are allowed: users can pass additional social platforms
59
+ */
60
+ [customAuthorSocialPlatform: string]: string;
61
+ };
62
+
63
+ export type AuthorAttributes = {
47
64
  /**
48
65
  * If `name` doesn't exist, an `imageURL` is expected.
49
66
  */
@@ -68,10 +85,48 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
68
85
  */
69
86
  email?: string;
70
87
  /**
71
- * Unknown keys are allowed, so that we can pass custom fields to authors,
72
- * e.g., `twitter`.
88
+ * Social platforms of the author
89
+ * Usually displayed as a list of social icon links.
90
+ */
91
+ socials?: AuthorSocials;
92
+ /**
93
+ * Description of the author.
94
+ */
95
+ description?: string;
96
+ /**
97
+ * Unknown keys are allowed, so that we can pass custom fields to authors.
98
+ */
99
+ [customAuthorAttribute: string]: unknown;
100
+ };
101
+
102
+ /**
103
+ * Metadata of the author's page, if it exists.
104
+ */
105
+ export type AuthorPage = {permalink: string};
106
+
107
+ /**
108
+ * Normalized author metadata.
109
+ */
110
+ export type Author = AuthorAttributes & {
111
+ /**
112
+ * Author key, if the author was loaded from the authors map.
113
+ * `null` means the author was declared inline.
114
+ */
115
+ key: string | null;
116
+ /**
117
+ * Metadata of the author's page.
118
+ * `null` means the author doesn't have a dedicated author page.
73
119
  */
74
- [key: string]: unknown;
120
+ page: AuthorPage | null;
121
+ };
122
+
123
+ /** Authors coming from the AuthorsMap always have a key */
124
+ export type AuthorWithKey = Author & {key: string};
125
+
126
+ /** What the authors list page should know about each author. */
127
+ export type AuthorItemProp = AuthorWithKey & {
128
+ /** Number of blog posts with this author. */
129
+ count: number;
75
130
  };
76
131
 
77
132
  /**
@@ -165,7 +220,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
165
220
  last_update?: FrontMatterLastUpdate;
166
221
  };
167
222
 
168
- export type BlogPostFrontMatterAuthor = Author & {
223
+ export type BlogPostFrontMatterAuthor = AuthorAttributes & {
169
224
  /**
170
225
  * Will be normalized into the `imageURL` prop.
171
226
  */
@@ -260,10 +315,26 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
260
315
  }) => string | undefined;
261
316
 
262
317
  export type FeedType = 'rss' | 'atom' | 'json';
318
+
319
+ export type FeedXSLTOptions = {
320
+ /**
321
+ * RSS XSLT file path, relative to the blog content folder.
322
+ * If null, no XSLT file is used and the feed will be displayed as raw XML.
323
+ */
324
+ rss: string | null;
325
+ /**
326
+ * Atom XSLT file path, relative to the blog content folder.
327
+ * If null, no XSLT file is used and the feed will be displayed as raw XML.
328
+ */
329
+ atom: string | null;
330
+ };
331
+
263
332
  /**
264
333
  * Normalized feed options used within code.
265
334
  */
266
335
  export type FeedOptions = {
336
+ /** Enable feeds xslt stylesheets */
337
+ xslt: FeedXSLTOptions;
267
338
  /** If `null`, no feed is generated. */
268
339
  type?: FeedType[] | null;
269
340
  /** Title of generated feed. */
@@ -398,6 +469,10 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
398
469
  blogTagsListComponent: string;
399
470
  /** Root component of the "posts containing tag" page. */
400
471
  blogTagsPostsComponent: string;
472
+ /** Root component of the authors list page. */
473
+ blogAuthorsListComponent: string;
474
+ /** Root component of the "posts containing author" page. */
475
+ blogAuthorsPostsComponent: string;
401
476
  /** Root component of the blog archive page. */
402
477
  blogArchiveComponent: string;
403
478
  /** Blog page title for better SEO. */
@@ -442,8 +517,22 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
442
517
  * (filter, modify, delete, etc...).
443
518
  */
444
519
  processBlogPosts: ProcessBlogPostsFn;
520
+ /* Base path for the authors page */
521
+ authorsBasePath: string;
522
+ /** The behavior of Docusaurus when it finds inline authors. */
523
+ onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw';
524
+ /** The behavior of Docusaurus when it finds untruncated blog posts. */
525
+ onUntruncatedBlogPosts: 'ignore' | 'log' | 'warn' | 'throw';
445
526
  };
446
527
 
528
+ export type UserFeedXSLTOptions =
529
+ | boolean
530
+ | null
531
+ | {
532
+ rss?: string | boolean | null;
533
+ atom?: string | boolean | null;
534
+ };
535
+
447
536
  /**
448
537
  * Feed options, as provided by user config. `type` accepts `all` as shortcut
449
538
  */
@@ -452,6 +541,8 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
452
541
  {
453
542
  /** Type of feed to be generated. Use `null` to disable generation. */
454
543
  type?: FeedOptions['type'] | 'all' | FeedType;
544
+ /** User-provided XSLT config for feeds, un-normalized */
545
+ xslt?: UserFeedXSLTOptions;
455
546
  }
456
547
  >;
457
548
  /**
@@ -469,6 +560,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
469
560
  title: string;
470
561
  permalink: string;
471
562
  unlisted: boolean;
563
+ date: Date | string;
472
564
  };
473
565
 
474
566
  export type BlogSidebar = {
@@ -476,17 +568,22 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
476
568
  items: BlogSidebarItem[];
477
569
  };
478
570
 
571
+ export type AuthorsMap = {[authorKey: string]: AuthorWithKey};
572
+
479
573
  export type BlogContent = {
480
574
  blogSidebarTitle: string;
481
575
  blogPosts: BlogPost[];
482
576
  blogListPaginated: BlogPaginated[];
483
577
  blogTags: BlogTags;
484
578
  blogTagsListPath: string;
579
+ authorsMap?: AuthorsMap;
485
580
  };
486
581
 
487
582
  export type BlogMetadata = {
488
583
  /** the path to the base of the blog */
489
584
  blogBasePath: string;
585
+ /** the path to the authors list page */
586
+ authorsListPath: string;
490
587
  /** title of the overall blog */
491
588
  blogTitle: string;
492
589
  };
@@ -647,6 +744,47 @@ declare module '@theme/BlogTagsListPage' {
647
744
  export default function BlogTagsListPage(props: Props): JSX.Element;
648
745
  }
649
746
 
747
+ declare module '@theme/Blog/Pages/BlogAuthorsListPage' {
748
+ import type {
749
+ AuthorItemProp,
750
+ BlogSidebar,
751
+ } from '@docusaurus/plugin-content-blog';
752
+
753
+ export interface Props {
754
+ /** Blog sidebar. */
755
+ readonly sidebar: BlogSidebar;
756
+ /** All authors declared in this blog. */
757
+ readonly authors: AuthorItemProp[];
758
+ }
759
+
760
+ export default function BlogAuthorsListPage(props: Props): JSX.Element;
761
+ }
762
+
763
+ declare module '@theme/Blog/Pages/BlogAuthorsPostsPage' {
764
+ import type {Content} from '@theme/BlogPostPage';
765
+ import type {
766
+ AuthorItemProp,
767
+ BlogSidebar,
768
+ BlogPaginatedMetadata,
769
+ } from '@docusaurus/plugin-content-blog';
770
+
771
+ export interface Props {
772
+ /** Blog sidebar. */
773
+ readonly sidebar: BlogSidebar;
774
+ /** Metadata of this author. */
775
+ readonly author: AuthorItemProp;
776
+ /** Looks exactly the same as the posts list page */
777
+ readonly listMetadata: BlogPaginatedMetadata;
778
+ /**
779
+ * Array of blog posts included on this page. Every post's metadata is also
780
+ * available.
781
+ */
782
+ readonly items: readonly {readonly content: Content}[];
783
+ }
784
+
785
+ export default function BlogAuthorsPostsPage(props: Props): JSX.Element;
786
+ }
787
+
650
788
  declare module '@theme/BlogTagsPostsPage' {
651
789
  import type {Content} from '@theme/BlogPostPage';
652
790
  import type {
package/src/props.ts CHANGED
@@ -5,7 +5,14 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import type {TagsListItem, TagModule} from '@docusaurus/utils';
8
- import type {BlogTag, BlogTags} from '@docusaurus/plugin-content-blog';
8
+ import type {
9
+ AuthorItemProp,
10
+ AuthorWithKey,
11
+ BlogPost,
12
+ BlogSidebar,
13
+ BlogTag,
14
+ BlogTags,
15
+ } from '@docusaurus/plugin-content-blog';
9
16
 
10
17
  export function toTagsProp({blogTags}: {blogTags: BlogTags}): TagsListItem[] {
11
18
  return Object.values(blogTags)
@@ -34,3 +41,34 @@ export function toTagProp({
34
41
  unlisted: tag.unlisted,
35
42
  };
36
43
  }
44
+
45
+ export function toAuthorItemProp({
46
+ author,
47
+ count,
48
+ }: {
49
+ author: AuthorWithKey;
50
+ count: number;
51
+ }): AuthorItemProp {
52
+ return {
53
+ ...author,
54
+ count,
55
+ };
56
+ }
57
+
58
+ export function toBlogSidebarProp({
59
+ blogSidebarTitle,
60
+ blogPosts,
61
+ }: {
62
+ blogSidebarTitle: string;
63
+ blogPosts: BlogPost[];
64
+ }): BlogSidebar {
65
+ return {
66
+ title: blogSidebarTitle,
67
+ items: blogPosts.map((blogPost) => ({
68
+ title: blogPost.metadata.title,
69
+ permalink: blogPost.metadata.permalink,
70
+ unlisted: blogPost.metadata.unlisted,
71
+ date: blogPost.metadata.date,
72
+ })),
73
+ };
74
+ }
package/src/routes.ts CHANGED
@@ -11,9 +11,15 @@ import {
11
11
  docuHash,
12
12
  aliasedSitePathToRelativePath,
13
13
  } from '@docusaurus/utils';
14
- import {shouldBeListed} from './blogUtils';
14
+ import {paginateBlogPosts, shouldBeListed} from './blogUtils';
15
15
 
16
- import {toTagProp, toTagsProp} from './props';
16
+ import {
17
+ toAuthorItemProp,
18
+ toBlogSidebarProp,
19
+ toTagProp,
20
+ toTagsProp,
21
+ } from './props';
22
+ import {groupBlogPostsByAuthorKey} from './authors';
17
23
  import type {
18
24
  PluginContentLoadedActions,
19
25
  RouteConfig,
@@ -26,7 +32,7 @@ import type {
26
32
  BlogContent,
27
33
  PluginOptions,
28
34
  BlogPost,
29
- BlogSidebar,
35
+ AuthorWithKey,
30
36
  } from '@docusaurus/plugin-content-blog';
31
37
 
32
38
  type CreateAllRoutesParam = {
@@ -55,11 +61,16 @@ export async function buildAllRoutes({
55
61
  blogListComponent,
56
62
  blogPostComponent,
57
63
  blogTagsListComponent,
64
+ blogAuthorsListComponent,
65
+ blogAuthorsPostsComponent,
58
66
  blogTagsPostsComponent,
59
67
  blogArchiveComponent,
60
68
  routeBasePath,
61
69
  archiveBasePath,
62
70
  blogTitle,
71
+ authorsBasePath,
72
+ postsPerPage,
73
+ blogDescription,
63
74
  } = options;
64
75
  const pluginId = options.id!;
65
76
  const {createData} = actions;
@@ -69,8 +80,15 @@ export async function buildAllRoutes({
69
80
  blogListPaginated,
70
81
  blogTags,
71
82
  blogTagsListPath,
83
+ authorsMap,
72
84
  } = content;
73
85
 
86
+ const authorsListPath = normalizeUrl([
87
+ baseUrl,
88
+ routeBasePath,
89
+ authorsBasePath,
90
+ ]);
91
+
74
92
  const listedBlogPosts = blogPosts.filter(shouldBeListed);
75
93
 
76
94
  const blogPostsById = _.keyBy(blogPosts, (post) => post.id);
@@ -88,17 +106,13 @@ export async function buildAllRoutes({
88
106
  : blogPosts.slice(0, options.blogSidebarCount);
89
107
 
90
108
  async function createSidebarModule() {
91
- const sidebar: BlogSidebar = {
92
- title: blogSidebarTitle,
93
- items: sidebarBlogPosts.map((blogPost) => ({
94
- title: blogPost.metadata.title,
95
- permalink: blogPost.metadata.permalink,
96
- unlisted: blogPost.metadata.unlisted,
97
- })),
98
- };
109
+ const sidebarProp = toBlogSidebarProp({
110
+ blogSidebarTitle,
111
+ blogPosts: sidebarBlogPosts,
112
+ });
99
113
  const modulePath = await createData(
100
114
  `blog-post-list-prop-${pluginId}.json`,
101
- sidebar,
115
+ sidebarProp,
102
116
  );
103
117
  return aliasedSource(modulePath);
104
118
  }
@@ -107,6 +121,7 @@ export async function buildAllRoutes({
107
121
  const blogMetadata: BlogMetadata = {
108
122
  blogBasePath: normalizeUrl([baseUrl, routeBasePath]),
109
123
  blogTitle,
124
+ authorsListPath,
110
125
  };
111
126
  const modulePath = await createData(
112
127
  `blogMetadata-${pluginId}.json`,
@@ -254,10 +269,85 @@ export async function buildAllRoutes({
254
269
  return [tagsListRoute, ...tagsPaginatedRoutes];
255
270
  }
256
271
 
272
+ function createAuthorsRoutes(): RouteConfig[] {
273
+ if (authorsMap === undefined || Object.keys(authorsMap).length === 0) {
274
+ return [];
275
+ }
276
+
277
+ const blogPostsByAuthorKey = groupBlogPostsByAuthorKey({
278
+ authorsMap,
279
+ blogPosts,
280
+ });
281
+ const authors = Object.values(authorsMap);
282
+
283
+ return [
284
+ createAuthorListRoute(),
285
+ ...authors.flatMap(createAuthorPaginatedRoute),
286
+ ];
287
+
288
+ function createAuthorListRoute(): RouteConfig {
289
+ return {
290
+ path: authorsListPath,
291
+ component: blogAuthorsListComponent,
292
+ exact: true,
293
+ modules: {
294
+ sidebar: sidebarModulePath,
295
+ },
296
+ props: {
297
+ authors: authors.map((author) =>
298
+ toAuthorItemProp({
299
+ author,
300
+ count: blogPostsByAuthorKey[author.key]?.length ?? 0,
301
+ }),
302
+ ),
303
+ },
304
+ context: {
305
+ blogMetadata: blogMetadataModulePath,
306
+ },
307
+ };
308
+ }
309
+
310
+ function createAuthorPaginatedRoute(author: AuthorWithKey): RouteConfig[] {
311
+ const authorBlogPosts = blogPostsByAuthorKey[author.key] ?? [];
312
+ if (!author.page) {
313
+ return [];
314
+ }
315
+
316
+ const pages = paginateBlogPosts({
317
+ blogPosts: authorBlogPosts,
318
+ basePageUrl: author.page.permalink,
319
+ blogDescription,
320
+ blogTitle,
321
+ pageBasePath: authorsBasePath,
322
+ postsPerPageOption: postsPerPage,
323
+ });
324
+
325
+ return pages.map(({metadata, items}) => {
326
+ return {
327
+ path: metadata.permalink,
328
+ component: blogAuthorsPostsComponent,
329
+ exact: true,
330
+ modules: {
331
+ items: blogPostItemsModule(items),
332
+ sidebar: sidebarModulePath,
333
+ },
334
+ props: {
335
+ author: toAuthorItemProp({author, count: authorBlogPosts.length}),
336
+ listMetadata: metadata,
337
+ },
338
+ context: {
339
+ blogMetadata: blogMetadataModulePath,
340
+ },
341
+ };
342
+ });
343
+ }
344
+ }
345
+
257
346
  return [
258
347
  ...createBlogPostRoutes(),
259
348
  ...createBlogPostsPaginatedRoutes(),
260
349
  ...createTagsRoutes(),
261
350
  ...createArchiveRoute(),
351
+ ...createAuthorsRoutes(),
262
352
  ];
263
353
  }
@@ -1,20 +0,0 @@
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 useRouteContext from '@docusaurus/useRouteContext';
9
- import type {BlogMetadata} from '@docusaurus/plugin-content-blog';
10
-
11
- export function useBlogMetadata(): BlogMetadata {
12
- const routeContext = useRouteContext();
13
- const blogMetadata = routeContext?.data?.blogMetadata;
14
- if (!blogMetadata) {
15
- throw new Error(
16
- "useBlogMetadata() can't be called on the current route because the blog metadata could not be found in route context",
17
- );
18
- }
19
- return blogMetadata as BlogMetadata;
20
- }