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

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/authors.ts CHANGED
@@ -5,83 +5,16 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import * as _ from 'lodash';
9
- import {getDataFileData, normalizeUrl} from '@docusaurus/utils';
10
- import {Joi, URISchema} from '@docusaurus/utils-validation';
11
- import {AuthorSocialsSchema, normalizeSocials} from './authorsSocials';
12
- import type {BlogContentPaths} from './types';
8
+ import _ from 'lodash';
9
+ import {normalizeUrl} from '@docusaurus/utils';
13
10
  import type {
14
11
  Author,
12
+ AuthorsMap,
13
+ BlogPost,
15
14
  BlogPostFrontMatter,
16
15
  BlogPostFrontMatterAuthor,
17
- BlogPostFrontMatterAuthors,
18
16
  } from '@docusaurus/plugin-content-blog';
19
17
 
20
- export type AuthorsMap = {[authorKey: string]: Author};
21
-
22
- const AuthorsMapSchema = Joi.object<AuthorsMap>()
23
- .pattern(
24
- Joi.string(),
25
- Joi.object<Author>({
26
- name: Joi.string(),
27
- url: URISchema,
28
- imageURL: URISchema,
29
- title: Joi.string(),
30
- email: Joi.string(),
31
- socials: AuthorSocialsSchema,
32
- })
33
- .rename('image_url', 'imageURL')
34
- .or('name', 'imageURL')
35
- .unknown()
36
- .required()
37
- .messages({
38
- 'object.base':
39
- '{#label} should be an author object containing properties like name, title, and imageURL.',
40
- 'any.required':
41
- '{#label} cannot be undefined. It should be an author object containing properties like name, title, and imageURL.',
42
- }),
43
- )
44
- .messages({
45
- 'object.base':
46
- "The authors map file should contain an object where each entry contains an author key and the corresponding author's data.",
47
- });
48
-
49
- export function validateAuthorsMap(content: unknown): AuthorsMap {
50
- const {error, value} = AuthorsMapSchema.validate(content);
51
- if (error) {
52
- throw error;
53
- }
54
- return value;
55
- }
56
-
57
- function normalizeAuthor(author: Author): Author {
58
- return {
59
- ...author,
60
- socials: author.socials ? normalizeSocials(author.socials) : undefined,
61
- };
62
- }
63
-
64
- function normalizeAuthorsMap(authorsMap: AuthorsMap): AuthorsMap {
65
- return _.mapValues(authorsMap, normalizeAuthor);
66
- }
67
-
68
- export async function getAuthorsMap(params: {
69
- authorsMapPath: string;
70
- contentPaths: BlogContentPaths;
71
- }): Promise<AuthorsMap | undefined> {
72
- const authorsMap = await getDataFileData(
73
- {
74
- filePath: params.authorsMapPath,
75
- contentPaths: params.contentPaths,
76
- fileType: 'authors map',
77
- },
78
- // TODO annoying to test: tightly coupled FS reads + validation...
79
- validateAuthorsMap,
80
- );
81
-
82
- return authorsMap ? normalizeAuthorsMap(authorsMap) : undefined;
83
- }
84
-
85
18
  type AuthorsParam = {
86
19
  frontMatter: BlogPostFrontMatter;
87
20
  authorsMap: AuthorsMap | undefined;
@@ -102,6 +35,7 @@ function normalizeImageUrl({
102
35
 
103
36
  // Legacy v1/early-v2 front matter fields
104
37
  // We may want to deprecate those in favor of using only frontMatter.authors
38
+ // TODO Docusaurus v4: remove this legacy front matter
105
39
  function getFrontMatterAuthorLegacy({
106
40
  baseUrl,
107
41
  frontMatter,
@@ -123,37 +57,40 @@ function getFrontMatterAuthorLegacy({
123
57
  title,
124
58
  url,
125
59
  imageURL,
60
+ // legacy front matter authors do not have an author key/page
61
+ key: null,
62
+ page: null,
126
63
  };
127
64
  }
128
65
 
129
66
  return undefined;
130
67
  }
131
68
 
132
- function normalizeFrontMatterAuthors(
133
- frontMatterAuthors: BlogPostFrontMatterAuthors = [],
134
- ): BlogPostFrontMatterAuthor[] {
135
- function normalizeFrontMatterAuthor(
136
- authorInput: string | Author,
137
- ): BlogPostFrontMatterAuthor {
138
- if (typeof authorInput === 'string') {
139
- // Technically, we could allow users to provide an author's name here, but
140
- // we only support keys, otherwise, a typo in a key would fallback to
141
- // becoming a name and may end up unnoticed
142
- return {key: authorInput};
69
+ function getFrontMatterAuthors(params: AuthorsParam): Author[] {
70
+ const {authorsMap, frontMatter, baseUrl} = params;
71
+ return normalizeFrontMatterAuthors().map(toAuthor);
72
+
73
+ function normalizeFrontMatterAuthors(): BlogPostFrontMatterAuthor[] {
74
+ if (frontMatter.authors === undefined) {
75
+ return [];
143
76
  }
144
- return authorInput;
145
- }
146
77
 
147
- return Array.isArray(frontMatterAuthors)
148
- ? frontMatterAuthors.map(normalizeFrontMatterAuthor)
149
- : [normalizeFrontMatterAuthor(frontMatterAuthors)];
150
- }
78
+ function normalizeAuthor(
79
+ authorInput: string | BlogPostFrontMatterAuthor,
80
+ ): BlogPostFrontMatterAuthor {
81
+ if (typeof authorInput === 'string') {
82
+ // We could allow users to provide an author's name here, but we only
83
+ // support keys, otherwise, a typo in a key would fall back to
84
+ // becoming a name and may end up unnoticed
85
+ return {key: authorInput};
86
+ }
87
+ return authorInput;
88
+ }
151
89
 
152
- function getFrontMatterAuthors(params: AuthorsParam): Author[] {
153
- const {authorsMap} = params;
154
- const frontMatterAuthors = normalizeFrontMatterAuthors(
155
- params.frontMatter.authors,
156
- );
90
+ return Array.isArray(frontMatter.authors)
91
+ ? frontMatter.authors.map(normalizeAuthor)
92
+ : [normalizeAuthor(frontMatter.authors)];
93
+ }
157
94
 
158
95
  function getAuthorsMapAuthor(key: string | undefined): Author | undefined {
159
96
  if (key) {
@@ -175,36 +112,29 @@ ${Object.keys(authorsMap)
175
112
  }
176
113
 
177
114
  function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
178
- return normalizeAuthor({
115
+ const author = {
179
116
  // Author def from authorsMap can be locally overridden by front matter
180
117
  ...getAuthorsMapAuthor(frontMatterAuthor.key),
181
118
  ...frontMatterAuthor,
182
- });
183
- }
184
-
185
- return frontMatterAuthors.map(toAuthor);
186
- }
119
+ };
187
120
 
188
- function fixAuthorImageBaseURL(
189
- authors: Author[],
190
- {baseUrl}: {baseUrl: string},
191
- ) {
192
- return authors.map((author) => ({
193
- ...author,
194
- imageURL: normalizeImageUrl({imageURL: author.imageURL, baseUrl}),
195
- }));
121
+ return {
122
+ ...author,
123
+ key: author.key ?? null,
124
+ page: author.page ?? null,
125
+ imageURL: normalizeImageUrl({imageURL: author.imageURL, baseUrl}),
126
+ };
127
+ }
196
128
  }
197
129
 
198
130
  export function getBlogPostAuthors(params: AuthorsParam): Author[] {
199
131
  const authorLegacy = getFrontMatterAuthorLegacy(params);
200
132
  const authors = getFrontMatterAuthors(params);
201
133
 
202
- const updatedAuthors = fixAuthorImageBaseURL(authors, params);
203
-
204
134
  if (authorLegacy) {
205
135
  // Technically, we could allow mixing legacy/authors front matter, but do we
206
136
  // really want to?
207
- if (updatedAuthors.length > 0) {
137
+ if (authors.length > 0) {
208
138
  throw new Error(
209
139
  `To declare blog post authors, use the 'authors' front matter in priority.
210
140
  Don't mix 'authors' with other existing 'author_*' front matter. Choose one or the other, not both at the same time.`,
@@ -213,5 +143,21 @@ Don't mix 'authors' with other existing 'author_*' front matter. Choose one or t
213
143
  return [authorLegacy];
214
144
  }
215
145
 
216
- return updatedAuthors;
146
+ return authors;
147
+ }
148
+
149
+ /**
150
+ * Group blog posts by author key
151
+ * Blog posts with only inline authors are ignored
152
+ */
153
+ export function groupBlogPostsByAuthorKey({
154
+ blogPosts,
155
+ authorsMap,
156
+ }: {
157
+ blogPosts: BlogPost[];
158
+ authorsMap: AuthorsMap | undefined;
159
+ }): Record<string, BlogPost[]> {
160
+ return _.mapValues(authorsMap, (author, key) =>
161
+ blogPosts.filter((p) => p.metadata.authors.some((a) => a.key === key)),
162
+ );
217
163
  }
@@ -0,0 +1,171 @@
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 _ from 'lodash';
9
+ import {readDataFile, normalizeUrl} from '@docusaurus/utils';
10
+ import {Joi, URISchema} from '@docusaurus/utils-validation';
11
+ import {AuthorSocialsSchema, normalizeSocials} from './authorsSocials';
12
+ import type {BlogContentPaths} from './types';
13
+ import type {
14
+ Author,
15
+ AuthorAttributes,
16
+ AuthorPage,
17
+ AuthorsMap,
18
+ } from '@docusaurus/plugin-content-blog';
19
+
20
+ type AuthorInput = AuthorAttributes & {
21
+ page?: boolean | AuthorPage;
22
+ };
23
+
24
+ export type AuthorsMapInput = {[authorKey: string]: AuthorInput};
25
+
26
+ const AuthorPageSchema = Joi.object<AuthorPage>({
27
+ permalink: Joi.string().required(),
28
+ });
29
+
30
+ const AuthorsMapInputSchema = Joi.object<AuthorsMapInput>()
31
+ .pattern(
32
+ Joi.string(),
33
+ Joi.object({
34
+ name: Joi.string(),
35
+ url: URISchema,
36
+ imageURL: URISchema,
37
+ title: Joi.string(),
38
+ email: Joi.string(),
39
+ page: Joi.alternatives(Joi.bool(), AuthorPageSchema),
40
+ socials: AuthorSocialsSchema,
41
+ description: Joi.string(),
42
+ })
43
+ .rename('image_url', 'imageURL')
44
+ .or('name', 'imageURL')
45
+ .unknown()
46
+ .required()
47
+ .messages({
48
+ 'object.base':
49
+ '{#label} should be an author object containing properties like name, title, and imageURL.',
50
+ 'any.required':
51
+ '{#label} cannot be undefined. It should be an author object containing properties like name, title, and imageURL.',
52
+ }),
53
+ )
54
+ .messages({
55
+ 'object.base':
56
+ "The authors map file should contain an object where each entry contains an author key and the corresponding author's data.",
57
+ });
58
+
59
+ export function checkAuthorsMapPermalinkCollisions(
60
+ authorsMap: AuthorsMap | undefined,
61
+ ): void {
62
+ if (!authorsMap) {
63
+ return;
64
+ }
65
+
66
+ const permalinkCounts = _(authorsMap)
67
+ // Filter to keep only authors with a page
68
+ .pickBy((author) => !!author.page)
69
+ // Group authors by their permalink
70
+ .groupBy((author) => author.page?.permalink)
71
+ // Filter to keep only permalinks with more than one author
72
+ .pickBy((authors) => authors.length > 1)
73
+ // Transform the object into an array of [permalink, authors] pairs
74
+ .toPairs()
75
+ .value();
76
+
77
+ if (permalinkCounts.length > 0) {
78
+ const errorMessage = permalinkCounts
79
+ .map(
80
+ ([permalink, authors]) =>
81
+ `Permalink: ${permalink}\nAuthors: ${authors
82
+ .map((author) => author.name || 'Unknown')
83
+ .join(', ')}`,
84
+ )
85
+ .join('\n');
86
+
87
+ throw new Error(
88
+ `The following permalinks are duplicated:\n${errorMessage}`,
89
+ );
90
+ }
91
+ }
92
+
93
+ function normalizeAuthor({
94
+ authorsBaseRoutePath,
95
+ authorKey,
96
+ author,
97
+ }: {
98
+ authorsBaseRoutePath: string;
99
+ authorKey: string;
100
+ author: AuthorInput;
101
+ }): Author & {key: string} {
102
+ function getAuthorPage(): AuthorPage | null {
103
+ if (!author.page) {
104
+ return null;
105
+ }
106
+ const slug =
107
+ author.page === true ? _.kebabCase(authorKey) : author.page.permalink;
108
+ return {
109
+ permalink: normalizeUrl([authorsBaseRoutePath, slug]),
110
+ };
111
+ }
112
+
113
+ return {
114
+ ...author,
115
+ key: authorKey,
116
+ page: getAuthorPage(),
117
+ socials: author.socials ? normalizeSocials(author.socials) : undefined,
118
+ };
119
+ }
120
+
121
+ function normalizeAuthorsMap({
122
+ authorsBaseRoutePath,
123
+ authorsMapInput,
124
+ }: {
125
+ authorsBaseRoutePath: string;
126
+ authorsMapInput: AuthorsMapInput;
127
+ }): AuthorsMap {
128
+ return _.mapValues(authorsMapInput, (author, authorKey) => {
129
+ return normalizeAuthor({authorsBaseRoutePath, authorKey, author});
130
+ });
131
+ }
132
+
133
+ export function validateAuthorsMapInput(content: unknown): AuthorsMapInput {
134
+ const {error, value} = AuthorsMapInputSchema.validate(content);
135
+ if (error) {
136
+ throw error;
137
+ }
138
+ return value;
139
+ }
140
+
141
+ async function getAuthorsMapInput(params: {
142
+ authorsMapPath: string;
143
+ contentPaths: BlogContentPaths;
144
+ }): Promise<AuthorsMapInput | undefined> {
145
+ const content = await readDataFile({
146
+ filePath: params.authorsMapPath,
147
+ contentPaths: params.contentPaths,
148
+ });
149
+ return content ? validateAuthorsMapInput(content) : undefined;
150
+ }
151
+
152
+ export async function getAuthorsMap(params: {
153
+ authorsMapPath: string;
154
+ authorsBaseRoutePath: string;
155
+ contentPaths: BlogContentPaths;
156
+ }): Promise<AuthorsMap | undefined> {
157
+ const authorsMapInput = await getAuthorsMapInput(params);
158
+ if (!authorsMapInput) {
159
+ return undefined;
160
+ }
161
+ const authorsMap = normalizeAuthorsMap({authorsMapInput, ...params});
162
+ return authorsMap;
163
+ }
164
+
165
+ export function validateAuthorsMap(content: unknown): AuthorsMapInput {
166
+ const {error, value} = AuthorsMapInputSchema.validate(content);
167
+ if (error) {
168
+ throw error;
169
+ }
170
+ return value;
171
+ }
package/src/blogUtils.ts CHANGED
@@ -29,11 +29,12 @@ import {
29
29
  } from '@docusaurus/utils';
30
30
  import {getTagsFile} from '@docusaurus/utils-validation';
31
31
  import {validateBlogPostFrontMatter} from './frontMatter';
32
- import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
32
+ import {getBlogPostAuthors} from './authors';
33
33
  import {reportAuthorsProblems} from './authorsProblems';
34
34
  import type {TagsFile} from '@docusaurus/utils';
35
35
  import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
36
36
  import type {
37
+ AuthorsMap,
37
38
  PluginOptions,
38
39
  ReadingTimeFunction,
39
40
  BlogPost,
@@ -64,7 +65,7 @@ export function paginateBlogPosts({
64
65
  const totalCount = blogPosts.length;
65
66
  const postsPerPage =
66
67
  postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
67
- const numberOfPages = Math.ceil(totalCount / postsPerPage);
68
+ const numberOfPages = Math.max(1, Math.ceil(totalCount / postsPerPage));
68
69
 
69
70
  const pages: BlogPaginated[] = [];
70
71
 
@@ -366,6 +367,7 @@ export async function generateBlogPosts(
366
367
  contentPaths: BlogContentPaths,
367
368
  context: LoadContext,
368
369
  options: PluginOptions,
370
+ authorsMap?: AuthorsMap,
369
371
  ): Promise<BlogPost[]> {
370
372
  const {include, exclude} = options;
371
373
 
@@ -378,11 +380,6 @@ export async function generateBlogPosts(
378
380
  ignore: exclude,
379
381
  });
380
382
 
381
- const authorsMap = await getAuthorsMap({
382
- contentPaths,
383
- authorsMapPath: options.authorsMapPath,
384
- });
385
-
386
383
  const tagsFile = await getTagsFile({contentPaths, tags: options.tags});
387
384
 
388
385
  async function doProcessBlogSourceFile(blogSourceFile: string) {
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
 
package/src/options.ts CHANGED
@@ -34,6 +34,8 @@ export const DEFAULT_OPTIONS: PluginOptions = {
34
34
  showReadingTime: true,
35
35
  blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
36
36
  blogTagsListComponent: '@theme/BlogTagsListPage',
37
+ blogAuthorsPostsComponent: '@theme/Blog/Pages/BlogAuthorsPostsPage',
38
+ blogAuthorsListComponent: '@theme/Blog/Pages/BlogAuthorsListPage',
37
39
  blogPostComponent: '@theme/BlogPostPage',
38
40
  blogListComponent: '@theme/BlogListPage',
39
41
  blogArchiveComponent: '@theme/BlogArchivePage',
@@ -58,6 +60,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
58
60
  processBlogPosts: async () => undefined,
59
61
  onInlineTags: 'warn',
60
62
  tags: undefined,
63
+ authorsBasePath: 'authors',
61
64
  onInlineAuthors: 'warn',
62
65
  };
63
66
 
@@ -82,6 +85,12 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
82
85
  blogTagsPostsComponent: Joi.string().default(
83
86
  DEFAULT_OPTIONS.blogTagsPostsComponent,
84
87
  ),
88
+ blogAuthorsPostsComponent: Joi.string().default(
89
+ DEFAULT_OPTIONS.blogAuthorsPostsComponent,
90
+ ),
91
+ blogAuthorsListComponent: Joi.string().default(
92
+ DEFAULT_OPTIONS.blogAuthorsListComponent,
93
+ ),
85
94
  blogArchiveComponent: Joi.string().default(
86
95
  DEFAULT_OPTIONS.blogArchiveComponent,
87
96
  ),
@@ -157,6 +166,9 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
157
166
  .disallow('')
158
167
  .allow(null, false)
159
168
  .default(() => DEFAULT_OPTIONS.tags),
169
+ authorsBasePath: Joi.string()
170
+ .default(DEFAULT_OPTIONS.authorsBasePath)
171
+ .disallow(''),
160
172
  onInlineAuthors: Joi.string()
161
173
  .equal('ignore', 'log', 'warn', 'throw')
162
174
  .default(DEFAULT_OPTIONS.onInlineAuthors),