@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
package/src/feed.ts CHANGED
@@ -7,15 +7,20 @@
7
7
 
8
8
  import path from 'path';
9
9
  import fs from 'fs-extra';
10
- import logger from '@docusaurus/logger';
11
10
  import {Feed, type Author as FeedAuthor} from 'feed';
12
11
  import * as srcset from 'srcset';
13
- import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
12
+ import {
13
+ getDataFilePath,
14
+ normalizeUrl,
15
+ readOutputHTMLFile,
16
+ } from '@docusaurus/utils';
14
17
  import {
15
18
  blogPostContainerID,
16
19
  applyTrailingSlash,
17
20
  } from '@docusaurus/utils-common';
18
21
  import {load as cheerioLoad} from 'cheerio';
22
+ import logger from '@docusaurus/logger';
23
+ import type {BlogContentPaths} from './types';
19
24
  import type {DocusaurusConfig, HtmlTags, LoadContext} from '@docusaurus/types';
20
25
  import type {
21
26
  FeedType,
@@ -23,6 +28,8 @@ import type {
23
28
  Author,
24
29
  BlogPost,
25
30
  BlogFeedItem,
31
+ FeedOptions,
32
+ FeedXSLTOptions,
26
33
  } from '@docusaurus/plugin-content-blog';
27
34
 
28
35
  async function generateBlogFeed({
@@ -180,32 +187,144 @@ async function defaultCreateFeedItems({
180
187
  );
181
188
  }
182
189
 
190
+ async function resolveXsltFilePaths({
191
+ xsltFilePath,
192
+ contentPaths,
193
+ }: {
194
+ xsltFilePath: string;
195
+ contentPaths: BlogContentPaths;
196
+ }) {
197
+ const xsltAbsolutePath: string = path.isAbsolute(xsltFilePath)
198
+ ? xsltFilePath
199
+ : (await getDataFilePath({filePath: xsltFilePath, contentPaths})) ??
200
+ path.resolve(contentPaths.contentPath, xsltFilePath);
201
+
202
+ if (!(await fs.pathExists(xsltAbsolutePath))) {
203
+ throw new Error(
204
+ logger.interpolate`Blog feed XSLT file not found at path=${path.relative(
205
+ process.cwd(),
206
+ xsltAbsolutePath,
207
+ )}`,
208
+ );
209
+ }
210
+
211
+ const parsedPath = path.parse(xsltAbsolutePath);
212
+ const cssAbsolutePath = path.resolve(
213
+ parsedPath.dir,
214
+ `${parsedPath.name}.css`,
215
+ );
216
+ if (!(await fs.pathExists(xsltAbsolutePath))) {
217
+ throw new Error(
218
+ logger.interpolate`Blog feed XSLT file was found at path=${path.relative(
219
+ process.cwd(),
220
+ xsltAbsolutePath,
221
+ )}
222
+ But its expected co-located CSS file could not be found at path=${path.relative(
223
+ process.cwd(),
224
+ cssAbsolutePath,
225
+ )}
226
+ If you want to provide a custom XSLT file, you must provide a CSS file with the exact same name.`,
227
+ );
228
+ }
229
+
230
+ return {xsltAbsolutePath, cssAbsolutePath};
231
+ }
232
+
233
+ async function generateXsltFiles({
234
+ xsltFilePath,
235
+ generatePath,
236
+ contentPaths,
237
+ }: {
238
+ xsltFilePath: string;
239
+ generatePath: string;
240
+ contentPaths: BlogContentPaths;
241
+ }) {
242
+ const {xsltAbsolutePath, cssAbsolutePath} = await resolveXsltFilePaths({
243
+ xsltFilePath,
244
+ contentPaths,
245
+ });
246
+ const xsltOutputPath = path.join(
247
+ generatePath,
248
+ path.basename(xsltAbsolutePath),
249
+ );
250
+ const cssOutputPath = path.join(generatePath, path.basename(cssAbsolutePath));
251
+ await fs.copy(xsltAbsolutePath, xsltOutputPath);
252
+ await fs.copy(cssAbsolutePath, cssOutputPath);
253
+ }
254
+
255
+ // This modifies the XML feed content to add a relative href to the XSLT file
256
+ // Good enough for now: we probably don't need a full XML parser just for that
257
+ // See also https://darekkay.com/blog/rss-styling/
258
+ function injectXslt({
259
+ feedContent,
260
+ xsltFilePath,
261
+ }: {
262
+ feedContent: string;
263
+ xsltFilePath: string;
264
+ }) {
265
+ return feedContent.replace(
266
+ '<?xml version="1.0" encoding="utf-8"?>',
267
+ `<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="${path.basename(
268
+ xsltFilePath,
269
+ )}"?>`,
270
+ );
271
+ }
272
+
273
+ const FeedConfigs: Record<
274
+ FeedType,
275
+ {
276
+ outputFileName: string;
277
+ getContent: (feed: Feed) => string;
278
+ getXsltFilePath: (xslt: FeedXSLTOptions) => string | null;
279
+ }
280
+ > = {
281
+ rss: {
282
+ outputFileName: 'rss.xml',
283
+ getContent: (feed) => feed.rss2(),
284
+ getXsltFilePath: (xslt) => xslt.rss,
285
+ },
286
+ atom: {
287
+ outputFileName: 'atom.xml',
288
+ getContent: (feed) => feed.atom1(),
289
+ getXsltFilePath: (xslt) => xslt.atom,
290
+ },
291
+ json: {
292
+ outputFileName: 'feed.json',
293
+ getContent: (feed) => feed.json1(),
294
+ getXsltFilePath: () => null,
295
+ },
296
+ };
297
+
183
298
  async function createBlogFeedFile({
184
299
  feed,
185
300
  feedType,
186
301
  generatePath,
302
+ feedOptions,
303
+ contentPaths,
187
304
  }: {
188
305
  feed: Feed;
189
306
  feedType: FeedType;
190
307
  generatePath: string;
308
+ feedOptions: FeedOptions;
309
+ contentPaths: BlogContentPaths;
191
310
  }) {
192
- const [feedContent, feedPath] = (() => {
193
- switch (feedType) {
194
- case 'rss':
195
- return [feed.rss2(), 'rss.xml'];
196
- case 'json':
197
- return [feed.json1(), 'feed.json'];
198
- case 'atom':
199
- return [feed.atom1(), 'atom.xml'];
200
- default:
201
- throw new Error(`Feed type ${feedType} not supported.`);
202
- }
203
- })();
204
311
  try {
205
- await fs.outputFile(path.join(generatePath, feedPath), feedContent);
312
+ const feedConfig = FeedConfigs[feedType];
313
+
314
+ let feedContent = feedConfig.getContent(feed);
315
+
316
+ const xsltFilePath = feedConfig.getXsltFilePath(feedOptions.xslt);
317
+ if (xsltFilePath) {
318
+ await generateXsltFiles({xsltFilePath, contentPaths, generatePath});
319
+ feedContent = injectXslt({feedContent, xsltFilePath});
320
+ }
321
+
322
+ const outputPath = path.join(generatePath, feedConfig.outputFileName);
323
+ await fs.outputFile(outputPath, feedContent);
206
324
  } catch (err) {
207
- logger.error(`Generating ${feedType} feed failed.`);
208
- throw err;
325
+ throw new Error(`Generating ${feedType} feed failed.`, {
326
+ cause: err as Error,
327
+ });
209
328
  }
210
329
  }
211
330
 
@@ -222,12 +341,14 @@ export async function createBlogFeedFiles({
222
341
  siteConfig,
223
342
  outDir,
224
343
  locale,
344
+ contentPaths,
225
345
  }: {
226
346
  blogPosts: BlogPost[];
227
347
  options: PluginOptions;
228
348
  siteConfig: DocusaurusConfig;
229
349
  outDir: string;
230
350
  locale: string;
351
+ contentPaths: BlogContentPaths;
231
352
  }): Promise<void> {
232
353
  const blogPosts = allBlogPosts.filter(shouldBeInFeed);
233
354
 
@@ -250,6 +371,8 @@ export async function createBlogFeedFiles({
250
371
  feed,
251
372
  feedType,
252
373
  generatePath: path.join(outDir, options.routeBasePath),
374
+ feedOptions: options.feedOptions,
375
+ contentPaths,
253
376
  }),
254
377
  ),
255
378
  );
@@ -13,6 +13,7 @@ import {
13
13
  URISchema,
14
14
  validateFrontMatter,
15
15
  } from '@docusaurus/utils-validation';
16
+ import {AuthorSocialsSchema} from './authorsSocials';
16
17
  import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';
17
18
 
18
19
  const BlogPostFrontMatterAuthorSchema = Joi.object({
@@ -21,6 +22,7 @@ const BlogPostFrontMatterAuthorSchema = Joi.object({
21
22
  title: Joi.string(),
22
23
  url: URISchema,
23
24
  imageURL: Joi.string(),
25
+ socials: AuthorSocialsSchema,
24
26
  })
25
27
  .or('key', 'name', 'imageURL')
26
28
  .rename('image_url', 'imageURL', {alias: true});
package/src/index.ts CHANGED
@@ -28,12 +28,14 @@ import {
28
28
  shouldBeListed,
29
29
  applyProcessBlogPosts,
30
30
  generateBlogPosts,
31
+ reportUntruncatedBlogPosts,
31
32
  } from './blogUtils';
32
33
  import footnoteIDFixer from './remark/footnoteIDFixer';
33
34
  import {translateContent, getTranslationFiles} from './translations';
34
35
  import {createBlogFeedFiles, createFeedHtmlHeadTags} from './feed';
35
36
 
36
37
  import {createAllRoutes} from './routes';
38
+ import {checkAuthorsMapPermalinkCollisions, getAuthorsMap} from './authorsMap';
37
39
  import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
38
40
  import type {LoadContext, Plugin} from '@docusaurus/types';
39
41
  import type {
@@ -160,15 +162,38 @@ export default async function pluginContentBlog(
160
162
  blogTitle,
161
163
  blogSidebarTitle,
162
164
  pageBasePath,
165
+ authorsBasePath,
166
+ authorsMapPath,
163
167
  } = options;
164
168
 
165
169
  const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
166
170
  const blogTagsListPath = normalizeUrl([baseBlogUrl, tagsBasePath]);
167
- 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
+ );
168
189
  blogPosts = await applyProcessBlogPosts({
169
190
  blogPosts,
170
191
  processBlogPosts: options.processBlogPosts,
171
192
  });
193
+ reportUntruncatedBlogPosts({
194
+ blogPosts,
195
+ onUntruncatedBlogPosts: options.onUntruncatedBlogPosts,
196
+ });
172
197
  const listedBlogPosts = blogPosts.filter(shouldBeListed);
173
198
 
174
199
  if (!blogPosts.length) {
@@ -178,6 +203,7 @@ export default async function pluginContentBlog(
178
203
  blogListPaginated: [],
179
204
  blogTags: {},
180
205
  blogTagsListPath,
206
+ authorsMap,
181
207
  };
182
208
  }
183
209
 
@@ -226,6 +252,7 @@ export default async function pluginContentBlog(
226
252
  blogListPaginated,
227
253
  blogTags,
228
254
  blogTagsListPath,
255
+ authorsMap,
229
256
  };
230
257
  },
231
258
 
@@ -250,6 +277,7 @@ export default async function pluginContentBlog(
250
277
  admonitions,
251
278
  rehypePlugins,
252
279
  remarkPlugins,
280
+ recmaPlugins,
253
281
  truncateMarker,
254
282
  beforeDefaultRemarkPlugins,
255
283
  beforeDefaultRehypePlugins,
@@ -262,6 +290,7 @@ export default async function pluginContentBlog(
262
290
  admonitions,
263
291
  remarkPlugins,
264
292
  rehypePlugins,
293
+ recmaPlugins,
265
294
  beforeDefaultRemarkPlugins: [
266
295
  footnoteIDFixer,
267
296
  ...beforeDefaultRemarkPlugins,
@@ -364,6 +393,7 @@ export default async function pluginContentBlog(
364
393
  outDir,
365
394
  siteConfig,
366
395
  locale: currentLocale,
396
+ contentPaths,
367
397
  });
368
398
  },
369
399
 
package/src/options.ts CHANGED
@@ -5,10 +5,12 @@
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,
11
12
  RehypePluginsSchema,
13
+ RecmaPluginsSchema,
12
14
  AdmonitionsSchema,
13
15
  RouteBasePathSchema,
14
16
  URISchema,
@@ -18,20 +20,32 @@ import type {
18
20
  PluginOptions,
19
21
  Options,
20
22
  FeedType,
23
+ FeedXSLTOptions,
21
24
  } from '@docusaurus/plugin-content-blog';
22
25
  import type {OptionValidationContext} from '@docusaurus/types';
23
26
 
24
27
  export const DEFAULT_OPTIONS: PluginOptions = {
25
- 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
+ },
26
37
  beforeDefaultRehypePlugins: [],
27
38
  beforeDefaultRemarkPlugins: [],
28
39
  admonitions: true,
29
40
  truncateMarker: /<!--\s*truncate\s*-->|\{\/\*\s*truncate\s*\*\/\}/,
30
41
  rehypePlugins: [],
31
42
  remarkPlugins: [],
43
+ recmaPlugins: [],
32
44
  showReadingTime: true,
33
45
  blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
34
46
  blogTagsListComponent: '@theme/BlogTagsListPage',
47
+ blogAuthorsPostsComponent: '@theme/Blog/Pages/BlogAuthorsPostsPage',
48
+ blogAuthorsListComponent: '@theme/Blog/Pages/BlogAuthorsListPage',
35
49
  blogPostComponent: '@theme/BlogPostPage',
36
50
  blogListComponent: '@theme/BlogListPage',
37
51
  blogArchiveComponent: '@theme/BlogArchivePage',
@@ -56,8 +70,99 @@ export const DEFAULT_OPTIONS: PluginOptions = {
56
70
  processBlogPosts: async () => undefined,
57
71
  onInlineTags: 'warn',
58
72
  tags: undefined,
73
+ authorsBasePath: 'authors',
74
+ onInlineAuthors: 'warn',
75
+ onUntruncatedBlogPosts: 'warn',
59
76
  };
60
77
 
78
+ export const XSLTBuiltInPaths = {
79
+ rss: path.resolve(__dirname, '..', 'assets', 'rss.xsl'),
80
+ atom: path.resolve(__dirname, '..', 'assets', 'atom.xsl'),
81
+ };
82
+
83
+ function normalizeXsltOption(
84
+ option: string | null | boolean,
85
+ type: 'rss' | 'atom',
86
+ ): string | null {
87
+ if (typeof option === 'string') {
88
+ return option;
89
+ }
90
+ if (option === true) {
91
+ return XSLTBuiltInPaths[type];
92
+ }
93
+ return null;
94
+ }
95
+
96
+ function createXSLTFilePathSchema(type: 'atom' | 'rss') {
97
+ return Joi.alternatives()
98
+ .try(
99
+ Joi.string().required(),
100
+ Joi.boolean()
101
+ .allow(null, () => undefined)
102
+ .custom((val) => normalizeXsltOption(val, type)),
103
+ )
104
+ .optional()
105
+ .default(null);
106
+ }
107
+
108
+ const FeedXSLTOptionsSchema = Joi.alternatives()
109
+ .try(
110
+ Joi.object<FeedXSLTOptions>({
111
+ rss: createXSLTFilePathSchema('rss'),
112
+ atom: createXSLTFilePathSchema('atom'),
113
+ }).required(),
114
+ Joi.boolean()
115
+ .allow(null, () => undefined)
116
+ .custom((val) => ({
117
+ rss: normalizeXsltOption(val, 'rss'),
118
+ atom: normalizeXsltOption(val, 'atom'),
119
+ })),
120
+ )
121
+ .optional()
122
+ .custom((val) => {
123
+ if (val === null) {
124
+ return {
125
+ rss: null,
126
+ atom: null,
127
+ };
128
+ }
129
+ return val;
130
+ })
131
+ .default(DEFAULT_OPTIONS.feedOptions.xslt);
132
+
133
+ const FeedOptionsSchema = Joi.object({
134
+ type: Joi.alternatives()
135
+ .try(
136
+ Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
137
+ Joi.alternatives().conditional(
138
+ Joi.string().equal('all', 'rss', 'atom', 'json'),
139
+ {
140
+ then: Joi.custom((val: FeedType | 'all') =>
141
+ val === 'all' ? ['rss', 'atom', 'json'] : [val],
142
+ ),
143
+ },
144
+ ),
145
+ )
146
+ .allow(null)
147
+ .default(DEFAULT_OPTIONS.feedOptions.type),
148
+ xslt: FeedXSLTOptionsSchema,
149
+ title: Joi.string().allow(''),
150
+ description: Joi.string().allow(''),
151
+ // Only add default value when user actually wants a feed (type is not null)
152
+ copyright: Joi.when('type', {
153
+ is: Joi.any().valid(null),
154
+ then: Joi.string().optional(),
155
+ otherwise: Joi.string()
156
+ .allow('')
157
+ .default(DEFAULT_OPTIONS.feedOptions.copyright),
158
+ }),
159
+ language: Joi.string(),
160
+ createFeedItems: Joi.function(),
161
+ limit: Joi.alternatives()
162
+ .try(Joi.number(), Joi.valid(null), Joi.valid(false))
163
+ .default(DEFAULT_OPTIONS.feedOptions.limit),
164
+ }).default(DEFAULT_OPTIONS.feedOptions);
165
+
61
166
  const PluginOptionSchema = Joi.object<PluginOptions>({
62
167
  path: Joi.string().default(DEFAULT_OPTIONS.path),
63
168
  archiveBasePath: Joi.string()
@@ -79,6 +184,12 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
79
184
  blogTagsPostsComponent: Joi.string().default(
80
185
  DEFAULT_OPTIONS.blogTagsPostsComponent,
81
186
  ),
187
+ blogAuthorsPostsComponent: Joi.string().default(
188
+ DEFAULT_OPTIONS.blogAuthorsPostsComponent,
189
+ ),
190
+ blogAuthorsListComponent: Joi.string().default(
191
+ DEFAULT_OPTIONS.blogAuthorsListComponent,
192
+ ),
82
193
  blogArchiveComponent: Joi.string().default(
83
194
  DEFAULT_OPTIONS.blogArchiveComponent,
84
195
  ),
@@ -93,6 +204,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
93
204
  showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime),
94
205
  remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
95
206
  rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
207
+ recmaPlugins: RecmaPluginsSchema.default(DEFAULT_OPTIONS.recmaPlugins),
96
208
  admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
97
209
  editUrl: Joi.alternatives().try(URISchema, Joi.function()),
98
210
  editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles),
@@ -103,37 +215,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
103
215
  beforeDefaultRehypePlugins: RehypePluginsSchema.default(
104
216
  DEFAULT_OPTIONS.beforeDefaultRehypePlugins,
105
217
  ),
106
- feedOptions: Joi.object({
107
- type: Joi.alternatives()
108
- .try(
109
- Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
110
- Joi.alternatives().conditional(
111
- Joi.string().equal('all', 'rss', 'atom', 'json'),
112
- {
113
- then: Joi.custom((val: FeedType | 'all') =>
114
- val === 'all' ? ['rss', 'atom', 'json'] : [val],
115
- ),
116
- },
117
- ),
118
- )
119
- .allow(null)
120
- .default(DEFAULT_OPTIONS.feedOptions.type),
121
- title: Joi.string().allow(''),
122
- description: Joi.string().allow(''),
123
- // Only add default value when user actually wants a feed (type is not null)
124
- copyright: Joi.when('type', {
125
- is: Joi.any().valid(null),
126
- then: Joi.string().optional(),
127
- otherwise: Joi.string()
128
- .allow('')
129
- .default(DEFAULT_OPTIONS.feedOptions.copyright),
130
- }),
131
- language: Joi.string(),
132
- createFeedItems: Joi.function(),
133
- limit: Joi.alternatives()
134
- .try(Joi.number(), Joi.valid(null), Joi.valid(false))
135
- .default(DEFAULT_OPTIONS.feedOptions.limit),
136
- }).default(DEFAULT_OPTIONS.feedOptions),
218
+ feedOptions: FeedOptionsSchema,
137
219
  authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
138
220
  readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),
139
221
  sortPosts: Joi.string()
@@ -153,6 +235,15 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
153
235
  .disallow('')
154
236
  .allow(null, false)
155
237
  .default(() => DEFAULT_OPTIONS.tags),
238
+ authorsBasePath: Joi.string()
239
+ .default(DEFAULT_OPTIONS.authorsBasePath)
240
+ .disallow(''),
241
+ onInlineAuthors: Joi.string()
242
+ .equal('ignore', 'log', 'warn', 'throw')
243
+ .default(DEFAULT_OPTIONS.onInlineAuthors),
244
+ onUntruncatedBlogPosts: Joi.string()
245
+ .equal('ignore', 'log', 'warn', 'throw')
246
+ .default(DEFAULT_OPTIONS.onUntruncatedBlogPosts),
156
247
  }).default(DEFAULT_OPTIONS);
157
248
 
158
249
  export function validateOptions({