@docusaurus/plugin-content-blog 0.0.0-6012 → 0.0.0-6016

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.
package/src/index.ts CHANGED
@@ -34,6 +34,7 @@ import {translateContent, getTranslationFiles} from './translations';
34
34
  import {createBlogFeedFiles, createFeedHtmlHeadTags} from './feed';
35
35
 
36
36
  import {createAllRoutes} from './routes';
37
+ import {checkAuthorsMapPermalinkCollisions, getAuthorsMap} from './authorsMap';
37
38
  import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
38
39
  import type {LoadContext, Plugin} from '@docusaurus/types';
39
40
  import type {
@@ -160,11 +161,30 @@ export default async function pluginContentBlog(
160
161
  blogTitle,
161
162
  blogSidebarTitle,
162
163
  pageBasePath,
164
+ authorsBasePath,
165
+ authorsMapPath,
163
166
  } = options;
164
167
 
165
168
  const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
166
169
  const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
167
- let blogPosts = await generateBlogPosts(contentPaths, context, options);
170
+
171
+ const authorsMap = await getAuthorsMap({
172
+ contentPaths,
173
+ authorsMapPath,
174
+ authorsBaseRoutePath: normalizeUrl([
175
+ baseUrl,
176
+ routeBasePath,
177
+ authorsBasePath,
178
+ ]),
179
+ });
180
+ checkAuthorsMapPermalinkCollisions(authorsMap);
181
+
182
+ let blogPosts = await generateBlogPosts(
183
+ contentPaths,
184
+ context,
185
+ options,
186
+ authorsMap,
187
+ );
168
188
  blogPosts = await applyProcessBlogPosts({
169
189
  blogPosts,
170
190
  processBlogPosts: options.processBlogPosts,
@@ -178,6 +198,7 @@ export default async function pluginContentBlog(
178
198
  blogListPaginated: [],
179
199
  blogTags: {},
180
200
  blogTagsListPath,
201
+ authorsMap,
181
202
  };
182
203
  }
183
204
 
@@ -226,6 +247,7 @@ export default async function pluginContentBlog(
226
247
  blogListPaginated,
227
248
  blogTags,
228
249
  blogTagsListPath,
250
+ authorsMap,
229
251
  };
230
252
  },
231
253
 
@@ -366,6 +388,7 @@ export default async function pluginContentBlog(
366
388
  outDir,
367
389
  siteConfig,
368
390
  locale: currentLocale,
391
+ contentPaths,
369
392
  });
370
393
  },
371
394
 
package/src/options.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
+ import path from 'path';
8
9
  import {
9
10
  Joi,
10
11
  RemarkPluginsSchema,
@@ -19,11 +20,20 @@ import type {
19
20
  PluginOptions,
20
21
  Options,
21
22
  FeedType,
23
+ FeedXSLTOptions,
22
24
  } from '@docusaurus/plugin-content-blog';
23
25
  import type {OptionValidationContext} from '@docusaurus/types';
24
26
 
25
27
  export const DEFAULT_OPTIONS: PluginOptions = {
26
- feedOptions: {type: ['rss', 'atom'], copyright: '', limit: 20},
28
+ feedOptions: {
29
+ type: ['rss', 'atom'],
30
+ copyright: '',
31
+ limit: 20,
32
+ xslt: {
33
+ rss: null,
34
+ atom: null,
35
+ },
36
+ },
27
37
  beforeDefaultRehypePlugins: [],
28
38
  beforeDefaultRemarkPlugins: [],
29
39
  admonitions: true,
@@ -34,6 +44,8 @@ export const DEFAULT_OPTIONS: PluginOptions = {
34
44
  showReadingTime: true,
35
45
  blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
36
46
  blogTagsListComponent: '@theme/BlogTagsListPage',
47
+ blogAuthorsPostsComponent: '@theme/Blog/Pages/BlogAuthorsPostsPage',
48
+ blogAuthorsListComponent: '@theme/Blog/Pages/BlogAuthorsListPage',
37
49
  blogPostComponent: '@theme/BlogPostPage',
38
50
  blogListComponent: '@theme/BlogListPage',
39
51
  blogArchiveComponent: '@theme/BlogArchivePage',
@@ -58,9 +70,98 @@ export const DEFAULT_OPTIONS: PluginOptions = {
58
70
  processBlogPosts: async () => undefined,
59
71
  onInlineTags: 'warn',
60
72
  tags: undefined,
73
+ authorsBasePath: 'authors',
61
74
  onInlineAuthors: 'warn',
62
75
  };
63
76
 
77
+ export const XSLTBuiltInPaths = {
78
+ rss: path.resolve(__dirname, '..', 'assets', 'rss.xsl'),
79
+ atom: path.resolve(__dirname, '..', 'assets', 'atom.xsl'),
80
+ };
81
+
82
+ function normalizeXsltOption(
83
+ option: string | null | boolean,
84
+ type: 'rss' | 'atom',
85
+ ): string | null {
86
+ if (typeof option === 'string') {
87
+ return option;
88
+ }
89
+ if (option === true) {
90
+ return XSLTBuiltInPaths[type];
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function createXSLTFilePathSchema(type: 'atom' | 'rss') {
96
+ return Joi.alternatives()
97
+ .try(
98
+ Joi.string().required(),
99
+ Joi.boolean()
100
+ .allow(null, () => undefined)
101
+ .custom((val) => normalizeXsltOption(val, type)),
102
+ )
103
+ .optional()
104
+ .default(null);
105
+ }
106
+
107
+ const FeedXSLTOptionsSchema = Joi.alternatives()
108
+ .try(
109
+ Joi.object<FeedXSLTOptions>({
110
+ rss: createXSLTFilePathSchema('rss'),
111
+ atom: createXSLTFilePathSchema('atom'),
112
+ }).required(),
113
+ Joi.boolean()
114
+ .allow(null, () => undefined)
115
+ .custom((val) => ({
116
+ rss: normalizeXsltOption(val, 'rss'),
117
+ atom: normalizeXsltOption(val, 'atom'),
118
+ })),
119
+ )
120
+ .optional()
121
+ .custom((val) => {
122
+ if (val === null) {
123
+ return {
124
+ rss: null,
125
+ atom: null,
126
+ };
127
+ }
128
+ return val;
129
+ })
130
+ .default(DEFAULT_OPTIONS.feedOptions.xslt);
131
+
132
+ const FeedOptionsSchema = Joi.object({
133
+ type: Joi.alternatives()
134
+ .try(
135
+ Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
136
+ Joi.alternatives().conditional(
137
+ Joi.string().equal('all', 'rss', 'atom', 'json'),
138
+ {
139
+ then: Joi.custom((val: FeedType | 'all') =>
140
+ val === 'all' ? ['rss', 'atom', 'json'] : [val],
141
+ ),
142
+ },
143
+ ),
144
+ )
145
+ .allow(null)
146
+ .default(DEFAULT_OPTIONS.feedOptions.type),
147
+ xslt: FeedXSLTOptionsSchema,
148
+ title: Joi.string().allow(''),
149
+ description: Joi.string().allow(''),
150
+ // Only add default value when user actually wants a feed (type is not null)
151
+ copyright: Joi.when('type', {
152
+ is: Joi.any().valid(null),
153
+ then: Joi.string().optional(),
154
+ otherwise: Joi.string()
155
+ .allow('')
156
+ .default(DEFAULT_OPTIONS.feedOptions.copyright),
157
+ }),
158
+ language: Joi.string(),
159
+ createFeedItems: Joi.function(),
160
+ limit: Joi.alternatives()
161
+ .try(Joi.number(), Joi.valid(null), Joi.valid(false))
162
+ .default(DEFAULT_OPTIONS.feedOptions.limit),
163
+ }).default(DEFAULT_OPTIONS.feedOptions);
164
+
64
165
  const PluginOptionSchema = Joi.object<PluginOptions>({
65
166
  path: Joi.string().default(DEFAULT_OPTIONS.path),
66
167
  archiveBasePath: Joi.string()
@@ -82,6 +183,12 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
82
183
  blogTagsPostsComponent: Joi.string().default(
83
184
  DEFAULT_OPTIONS.blogTagsPostsComponent,
84
185
  ),
186
+ blogAuthorsPostsComponent: Joi.string().default(
187
+ DEFAULT_OPTIONS.blogAuthorsPostsComponent,
188
+ ),
189
+ blogAuthorsListComponent: Joi.string().default(
190
+ DEFAULT_OPTIONS.blogAuthorsListComponent,
191
+ ),
85
192
  blogArchiveComponent: Joi.string().default(
86
193
  DEFAULT_OPTIONS.blogArchiveComponent,
87
194
  ),
@@ -107,37 +214,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
107
214
  beforeDefaultRehypePlugins: RehypePluginsSchema.default(
108
215
  DEFAULT_OPTIONS.beforeDefaultRehypePlugins,
109
216
  ),
110
- feedOptions: Joi.object({
111
- type: Joi.alternatives()
112
- .try(
113
- Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
114
- Joi.alternatives().conditional(
115
- Joi.string().equal('all', 'rss', 'atom', 'json'),
116
- {
117
- then: Joi.custom((val: FeedType | 'all') =>
118
- val === 'all' ? ['rss', 'atom', 'json'] : [val],
119
- ),
120
- },
121
- ),
122
- )
123
- .allow(null)
124
- .default(DEFAULT_OPTIONS.feedOptions.type),
125
- title: Joi.string().allow(''),
126
- description: Joi.string().allow(''),
127
- // Only add default value when user actually wants a feed (type is not null)
128
- copyright: Joi.when('type', {
129
- is: Joi.any().valid(null),
130
- then: Joi.string().optional(),
131
- otherwise: Joi.string()
132
- .allow('')
133
- .default(DEFAULT_OPTIONS.feedOptions.copyright),
134
- }),
135
- language: Joi.string(),
136
- createFeedItems: Joi.function(),
137
- limit: Joi.alternatives()
138
- .try(Joi.number(), Joi.valid(null), Joi.valid(false))
139
- .default(DEFAULT_OPTIONS.feedOptions.limit),
140
- }).default(DEFAULT_OPTIONS.feedOptions),
217
+ feedOptions: FeedOptionsSchema,
141
218
  authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
142
219
  readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),
143
220
  sortPosts: Joi.string()
@@ -157,6 +234,9 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
157
234
  .disallow('')
158
235
  .allow(null, false)
159
236
  .default(() => DEFAULT_OPTIONS.tags),
237
+ authorsBasePath: Joi.string()
238
+ .default(DEFAULT_OPTIONS.authorsBasePath)
239
+ .disallow(''),
160
240
  onInlineAuthors: Joi.string()
161
241
  .equal('ignore', 'log', 'warn', 'throw')
162
242
  .default(DEFAULT_OPTIONS.onInlineAuthors),
@@ -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
  */
@@ -66,9 +60,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
66
60
  [customAuthorSocialPlatform: string]: string;
67
61
  };
68
62
 
69
- export type Author = {
70
- key?: string; // TODO temporary, need refactor
71
-
63
+ export type AuthorAttributes = {
72
64
  /**
73
65
  * If `name` doesn't exist, an `imageURL` is expected.
74
66
  */
@@ -98,11 +90,45 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
98
90
  */
99
91
  socials?: AuthorSocials;
100
92
  /**
101
- * Unknown keys are allowed, so that we can pass custom fields to authors,
93
+ * Description of the author.
94
+ */
95
+ description?: string;
96
+ /**
97
+ * Unknown keys are allowed, so that we can pass custom fields to authors.
102
98
  */
103
99
  [customAuthorAttribute: string]: unknown;
104
100
  };
105
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.
119
+ */
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;
130
+ };
131
+
106
132
  /**
107
133
  * Everything is partial/unnormalized, because front matter is always
108
134
  * preserved as-is. Default values will be applied when generating metadata
@@ -194,7 +220,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
194
220
  last_update?: FrontMatterLastUpdate;
195
221
  };
196
222
 
197
- export type BlogPostFrontMatterAuthor = Author & {
223
+ export type BlogPostFrontMatterAuthor = AuthorAttributes & {
198
224
  /**
199
225
  * Will be normalized into the `imageURL` prop.
200
226
  */
@@ -289,10 +315,26 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
289
315
  }) => string | undefined;
290
316
 
291
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
+
292
332
  /**
293
333
  * Normalized feed options used within code.
294
334
  */
295
335
  export type FeedOptions = {
336
+ /** Enable feeds xslt stylesheets */
337
+ xslt: FeedXSLTOptions;
296
338
  /** If `null`, no feed is generated. */
297
339
  type?: FeedType[] | null;
298
340
  /** Title of generated feed. */
@@ -427,6 +469,10 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
427
469
  blogTagsListComponent: string;
428
470
  /** Root component of the "posts containing tag" page. */
429
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;
430
476
  /** Root component of the blog archive page. */
431
477
  blogArchiveComponent: string;
432
478
  /** Blog page title for better SEO. */
@@ -471,10 +517,20 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
471
517
  * (filter, modify, delete, etc...).
472
518
  */
473
519
  processBlogPosts: ProcessBlogPostsFn;
520
+ /* Base path for the authors page */
521
+ authorsBasePath: string;
474
522
  /** The behavior of Docusaurus when it finds inline authors. */
475
523
  onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw';
476
524
  };
477
525
 
526
+ export type UserFeedXSLTOptions =
527
+ | boolean
528
+ | null
529
+ | {
530
+ rss?: string | boolean | null;
531
+ atom?: string | boolean | null;
532
+ };
533
+
478
534
  /**
479
535
  * Feed options, as provided by user config. `type` accepts `all` as shortcut
480
536
  */
@@ -483,6 +539,8 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
483
539
  {
484
540
  /** Type of feed to be generated. Use `null` to disable generation. */
485
541
  type?: FeedOptions['type'] | 'all' | FeedType;
542
+ /** User-provided XSLT config for feeds, un-normalized */
543
+ xslt?: UserFeedXSLTOptions;
486
544
  }
487
545
  >;
488
546
  /**
@@ -508,17 +566,22 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
508
566
  items: BlogSidebarItem[];
509
567
  };
510
568
 
569
+ export type AuthorsMap = {[authorKey: string]: AuthorWithKey};
570
+
511
571
  export type BlogContent = {
512
572
  blogSidebarTitle: string;
513
573
  blogPosts: BlogPost[];
514
574
  blogListPaginated: BlogPaginated[];
515
575
  blogTags: BlogTags;
516
576
  blogTagsListPath: string;
577
+ authorsMap?: AuthorsMap;
517
578
  };
518
579
 
519
580
  export type BlogMetadata = {
520
581
  /** the path to the base of the blog */
521
582
  blogBasePath: string;
583
+ /** the path to the authors list page */
584
+ authorsListPath: string;
522
585
  /** title of the overall blog */
523
586
  blogTitle: string;
524
587
  };
@@ -679,6 +742,47 @@ declare module '@theme/BlogTagsListPage' {
679
742
  export default function BlogTagsListPage(props: Props): JSX.Element;
680
743
  }
681
744
 
745
+ declare module '@theme/Blog/Pages/BlogAuthorsListPage' {
746
+ import type {
747
+ AuthorItemProp,
748
+ BlogSidebar,
749
+ } from '@docusaurus/plugin-content-blog';
750
+
751
+ export interface Props {
752
+ /** Blog sidebar. */
753
+ readonly sidebar: BlogSidebar;
754
+ /** All authors declared in this blog. */
755
+ readonly authors: AuthorItemProp[];
756
+ }
757
+
758
+ export default function BlogAuthorsListPage(props: Props): JSX.Element;
759
+ }
760
+
761
+ declare module '@theme/Blog/Pages/BlogAuthorsPostsPage' {
762
+ import type {Content} from '@theme/BlogPostPage';
763
+ import type {
764
+ AuthorItemProp,
765
+ BlogSidebar,
766
+ BlogPaginatedMetadata,
767
+ } from '@docusaurus/plugin-content-blog';
768
+
769
+ export interface Props {
770
+ /** Blog sidebar. */
771
+ readonly sidebar: BlogSidebar;
772
+ /** Metadata of this author. */
773
+ readonly author: AuthorItemProp;
774
+ /** Looks exactly the same as the posts list page */
775
+ readonly listMetadata: BlogPaginatedMetadata;
776
+ /**
777
+ * Array of blog posts included on this page. Every post's metadata is also
778
+ * available.
779
+ */
780
+ readonly items: readonly {readonly content: Content}[];
781
+ }
782
+
783
+ export default function BlogAuthorsPostsPage(props: Props): JSX.Element;
784
+ }
785
+
682
786
  declare module '@theme/BlogTagsPostsPage' {
683
787
  import type {Content} from '@theme/BlogPostPage';
684
788
  import type {
package/src/props.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import type {TagsListItem, TagModule} from '@docusaurus/utils';
8
8
  import type {
9
+ AuthorItemProp,
10
+ AuthorWithKey,
9
11
  BlogPost,
10
12
  BlogSidebar,
11
13
  BlogTag,
@@ -40,6 +42,19 @@ export function toTagProp({
40
42
  };
41
43
  }
42
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
+
43
58
  export function toBlogSidebarProp({
44
59
  blogSidebarTitle,
45
60
  blogPosts,
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 {toBlogSidebarProp, 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,6 +32,7 @@ import type {
26
32
  BlogContent,
27
33
  PluginOptions,
28
34
  BlogPost,
35
+ AuthorWithKey,
29
36
  } from '@docusaurus/plugin-content-blog';
30
37
 
31
38
  type CreateAllRoutesParam = {
@@ -54,11 +61,16 @@ export async function buildAllRoutes({
54
61
  blogListComponent,
55
62
  blogPostComponent,
56
63
  blogTagsListComponent,
64
+ blogAuthorsListComponent,
65
+ blogAuthorsPostsComponent,
57
66
  blogTagsPostsComponent,
58
67
  blogArchiveComponent,
59
68
  routeBasePath,
60
69
  archiveBasePath,
61
70
  blogTitle,
71
+ authorsBasePath,
72
+ postsPerPage,
73
+ blogDescription,
62
74
  } = options;
63
75
  const pluginId = options.id!;
64
76
  const {createData} = actions;
@@ -68,8 +80,15 @@ export async function buildAllRoutes({
68
80
  blogListPaginated,
69
81
  blogTags,
70
82
  blogTagsListPath,
83
+ authorsMap,
71
84
  } = content;
72
85
 
86
+ const authorsListPath = normalizeUrl([
87
+ baseUrl,
88
+ routeBasePath,
89
+ authorsBasePath,
90
+ ]);
91
+
73
92
  const listedBlogPosts = blogPosts.filter(shouldBeListed);
74
93
 
75
94
  const blogPostsById = _.keyBy(blogPosts, (post) => post.id);
@@ -102,6 +121,7 @@ export async function buildAllRoutes({
102
121
  const blogMetadata: BlogMetadata = {
103
122
  blogBasePath: normalizeUrl([baseUrl, routeBasePath]),
104
123
  blogTitle,
124
+ authorsListPath,
105
125
  };
106
126
  const modulePath = await createData(
107
127
  `blogMetadata-${pluginId}.json`,
@@ -249,10 +269,85 @@ export async function buildAllRoutes({
249
269
  return [tagsListRoute, ...tagsPaginatedRoutes];
250
270
  }
251
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
+
252
346
  return [
253
347
  ...createBlogPostRoutes(),
254
348
  ...createBlogPostsPaginatedRoutes(),
255
349
  ...createTagsRoutes(),
256
350
  ...createArchiveRoute(),
351
+ ...createAuthorsRoutes(),
257
352
  ];
258
353
  }