@docusaurus/plugin-content-blog 2.0.0-beta.138b4c997 → 2.0.0-beta.14

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 (49) hide show
  1. package/lib/authors.d.ts +23 -0
  2. package/lib/authors.js +147 -0
  3. package/lib/blogFrontMatter.d.ts +19 -6
  4. package/lib/blogFrontMatter.js +31 -19
  5. package/lib/blogUtils.d.ts +10 -4
  6. package/lib/blogUtils.js +142 -137
  7. package/lib/feed.d.ts +20 -0
  8. package/lib/feed.js +105 -0
  9. package/lib/index.js +104 -106
  10. package/lib/markdownLoader.d.ts +3 -6
  11. package/lib/markdownLoader.js +5 -5
  12. package/lib/pluginOptionSchema.d.ts +3 -26
  13. package/lib/pluginOptionSchema.js +30 -9
  14. package/lib/translations.d.ts +10 -0
  15. package/lib/translations.js +53 -0
  16. package/lib/types.d.ts +55 -15
  17. package/package.json +17 -13
  18. package/src/authors.ts +196 -0
  19. package/src/blogFrontMatter.ts +71 -33
  20. package/src/blogUtils.ts +196 -181
  21. package/{types.d.ts → src/deps.d.ts} +0 -0
  22. package/src/feed.ts +149 -0
  23. package/src/index.ts +123 -107
  24. package/src/markdownLoader.ts +8 -12
  25. package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
  26. package/src/pluginOptionSchema.ts +34 -12
  27. package/src/translations.ts +63 -0
  28. package/src/types.ts +69 -16
  29. package/lib/.tsbuildinfo +0 -1
  30. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  31. package/src/__tests__/__fixtures__/website/blog/complex-slug.md +0 -7
  32. package/src/__tests__/__fixtures__/website/blog/date-matter.md +0 -5
  33. package/src/__tests__/__fixtures__/website/blog/draft.md +0 -6
  34. package/src/__tests__/__fixtures__/website/blog/heading-as-title.md +0 -5
  35. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +0 -7
  36. package/src/__tests__/__fixtures__/website/blog-with-ref/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  37. package/src/__tests__/__fixtures__/website/blog-with-ref/post-with-broken-links.md +0 -11
  38. package/src/__tests__/__fixtures__/website/blog-with-ref/post.md +0 -5
  39. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +0 -5
  40. package/src/__tests__/__fixtures__/website-blog-without-date/blog/no date.md +0 -1
  41. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -76
  42. package/src/__tests__/__snapshots__/linkify.test.ts.snap +0 -24
  43. package/src/__tests__/__snapshots__/pluginOptionSchema.test.ts.snap +0 -5
  44. package/src/__tests__/blogFrontMatter.test.ts +0 -317
  45. package/src/__tests__/generateBlogFeed.test.ts +0 -100
  46. package/src/__tests__/index.test.ts +0 -336
  47. package/src/__tests__/linkify.test.ts +0 -93
  48. package/src/__tests__/pluginOptionSchema.test.ts +0 -150
  49. package/tsconfig.json +0 -9
@@ -5,81 +5,119 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- /* eslint-disable camelcase */
9
-
10
8
  import {
11
9
  JoiFrontMatter as Joi, // Custom instance for frontmatter
12
10
  URISchema,
13
11
  validateFrontMatter,
12
+ FrontMatterTagsSchema,
13
+ FrontMatterTOCHeadingLevels,
14
14
  } from '@docusaurus/utils-validation';
15
- import {Tag} from './types';
15
+ import type {FrontMatterTag} from '@docusaurus/utils';
16
+
17
+ export type BlogPostFrontMatterAuthor = Record<string, unknown> & {
18
+ key?: string;
19
+ name?: string;
20
+ imageURL?: string;
21
+ url?: string;
22
+ title?: string;
23
+ };
24
+
25
+ // All the possible variants that the user can use for convenience
26
+ export type BlogPostFrontMatterAuthors =
27
+ | string
28
+ | BlogPostFrontMatterAuthor
29
+ | (string | BlogPostFrontMatterAuthor)[];
30
+
31
+ const BlogPostFrontMatterAuthorSchema = Joi.object({
32
+ key: Joi.string(),
33
+ name: Joi.string(),
34
+ title: Joi.string(),
35
+ url: URISchema,
36
+ imageURL: Joi.string(),
37
+ })
38
+ .or('key', 'name')
39
+ .rename('image_url', 'imageURL', {alias: true});
16
40
 
17
41
  export type BlogPostFrontMatter = {
18
42
  id?: string;
19
43
  title?: string;
20
44
  description?: string;
21
- tags?: (string | Tag)[];
45
+ tags?: FrontMatterTag[];
22
46
  slug?: string;
23
47
  draft?: boolean;
24
- date?: Date;
48
+ date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
49
+
50
+ authors?: BlogPostFrontMatterAuthors;
25
51
 
52
+ // We may want to deprecate those older author frontmatter fields later:
26
53
  author?: string;
27
54
  author_title?: string;
28
55
  author_url?: string;
29
56
  author_image_url?: string;
30
57
 
31
- image?: string;
32
- keywords?: string[];
33
- hide_table_of_contents?: boolean;
34
-
35
58
  /** @deprecated */
36
59
  authorTitle?: string;
60
+ /** @deprecated */
37
61
  authorURL?: string;
62
+ /** @deprecated */
38
63
  authorImageURL?: string;
64
+
65
+ image?: string;
66
+ keywords?: string[];
67
+ hide_table_of_contents?: boolean;
68
+ toc_min_heading_level?: number;
69
+ toc_max_heading_level?: number;
39
70
  };
40
71
 
41
- // NOTE: we don't add any default value on purpose here
42
- // We don't want default values to magically appear in doc metadatas and props
43
- // While the user did not provide those values explicitly
44
- // We use default values in code instead
45
- const BlogTagSchema = Joi.alternatives().try(
46
- Joi.string().required(),
47
- Joi.object<Tag>({
48
- label: Joi.string().required(),
49
- permalink: Joi.string().required(),
50
- }),
51
- );
72
+ const FrontMatterAuthorErrorMessage =
73
+ '{{#label}} does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).';
52
74
 
53
75
  const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
54
76
  id: Joi.string(),
55
77
  title: Joi.string().allow(''),
56
78
  description: Joi.string().allow(''),
57
- tags: Joi.array().items(BlogTagSchema),
79
+ tags: FrontMatterTagsSchema,
58
80
  draft: Joi.boolean(),
59
81
  date: Joi.date().raw(),
60
82
 
83
+ // New multi-authors frontmatter:
84
+ authors: Joi.alternatives()
85
+ .try(
86
+ Joi.string(),
87
+ BlogPostFrontMatterAuthorSchema,
88
+ Joi.array()
89
+ .items(Joi.string(), BlogPostFrontMatterAuthorSchema)
90
+ .messages({
91
+ 'array.sparse': FrontMatterAuthorErrorMessage,
92
+ 'array.includes': FrontMatterAuthorErrorMessage,
93
+ }),
94
+ )
95
+ .messages({
96
+ 'alternatives.match': FrontMatterAuthorErrorMessage,
97
+ }),
98
+ // Legacy author frontmatter
61
99
  author: Joi.string(),
62
100
  author_title: Joi.string(),
63
101
  author_url: URISchema,
64
102
  author_image_url: URISchema,
65
- slug: Joi.string(),
66
- image: URISchema,
67
- keywords: Joi.array().items(Joi.string().required()),
68
- hide_table_of_contents: Joi.boolean(),
69
-
70
- // TODO re-enable warnings later, our v1 blog posts use those older frontmatter fields
103
+ // TODO enable deprecation warnings later
71
104
  authorURL: URISchema,
72
105
  // .warning('deprecate.error', { alternative: '"author_url"'}),
73
106
  authorTitle: Joi.string(),
74
107
  // .warning('deprecate.error', { alternative: '"author_title"'}),
75
108
  authorImageURL: URISchema,
76
109
  // .warning('deprecate.error', { alternative: '"author_image_url"'}),
77
- })
78
- .unknown()
79
- .messages({
80
- 'deprecate.error':
81
- '{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
82
- });
110
+
111
+ slug: Joi.string(),
112
+ image: URISchema,
113
+ keywords: Joi.array().items(Joi.string().required()),
114
+ hide_table_of_contents: Joi.boolean(),
115
+
116
+ ...FrontMatterTOCHeadingLevels,
117
+ }).messages({
118
+ 'deprecate.error':
119
+ '{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
120
+ });
83
121
 
84
122
  export function validateBlogPostFrontMatter(
85
123
  frontMatter: Record<string, unknown>,
package/src/blogUtils.ts CHANGED
@@ -6,18 +6,16 @@
6
6
  */
7
7
 
8
8
  import fs from 'fs-extra';
9
- import globby from 'globby';
10
- import chalk from 'chalk';
11
9
  import path from 'path';
12
10
  import readingTime from 'reading-time';
13
- import {Feed} from 'feed';
14
11
  import {keyBy, mapValues} from 'lodash';
15
12
  import {
16
13
  PluginOptions,
17
14
  BlogPost,
18
- DateLink,
19
15
  BlogContentPaths,
20
16
  BlogMarkdownLoaderOptions,
17
+ BlogTags,
18
+ ReadingTimeFunction,
21
19
  } from './types';
22
20
  import {
23
21
  parseMarkdownFile,
@@ -27,9 +25,14 @@ import {
27
25
  getFolderContainingFile,
28
26
  posixPath,
29
27
  replaceMarkdownLinks,
28
+ Globby,
29
+ normalizeFrontMatterTags,
30
+ groupTaggedItems,
30
31
  } from '@docusaurus/utils';
31
32
  import {LoadContext} from '@docusaurus/types';
32
33
  import {validateBlogPostFrontMatter} from './blogFrontMatter';
34
+ import {AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
35
+ import logger from '@docusaurus/logger';
33
36
 
34
37
  export function truncate(fileString: string, truncateMarker: RegExp): string {
35
38
  return fileString.split(truncateMarker, 1).shift()!;
@@ -44,15 +47,44 @@ export function getSourceToPermalink(
44
47
  );
45
48
  }
46
49
 
47
- // YYYY-MM-DD-{name}.mdx?
48
- // Prefer named capture, but older Node versions do not support it.
49
- const DATE_FILENAME_PATTERN = /^(\d{4}-\d{1,2}-\d{1,2})-?(.*?).mdx?$/;
50
+ export function getBlogTags(blogPosts: BlogPost[]): BlogTags {
51
+ const groups = groupTaggedItems(
52
+ blogPosts,
53
+ (blogPost) => blogPost.metadata.tags,
54
+ );
55
+ return mapValues(groups, (group) => ({
56
+ name: group.tag.label,
57
+ items: group.items.map((item) => item.id),
58
+ permalink: group.tag.permalink,
59
+ }));
60
+ }
50
61
 
51
- function toUrl({date, link}: DateLink) {
52
- return `${date
53
- .toISOString()
54
- .substring(0, '2019-01-01'.length)
55
- .replace(/-/g, '/')}/${link}`;
62
+ const DATE_FILENAME_REGEX =
63
+ /^(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/;
64
+
65
+ type ParsedBlogFileName = {
66
+ date: Date | undefined;
67
+ text: string;
68
+ slug: string;
69
+ };
70
+
71
+ export function parseBlogFileName(
72
+ blogSourceRelative: string,
73
+ ): ParsedBlogFileName {
74
+ const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
75
+ if (dateFilenameMatch) {
76
+ const dateString = dateFilenameMatch.groups!.date!;
77
+ const text = dateFilenameMatch.groups!.text!;
78
+ // Always treat dates as UTC by adding the `Z`
79
+ const date = new Date(`${dateString}Z`);
80
+ const slugDate = dateString.replace(/-/g, '/');
81
+ const slug = `/${slugDate}/${text}`;
82
+ return {date, text, slug};
83
+ } else {
84
+ const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
85
+ const slug = `/${text}`;
86
+ return {date: undefined, text, slug};
87
+ }
56
88
  }
57
89
 
58
90
  function formatBlogPostDate(locale: string, date: Date): string {
@@ -68,209 +100,192 @@ function formatBlogPostDate(locale: string, date: Date): string {
68
100
  }
69
101
  }
70
102
 
71
- export async function generateBlogFeed(
72
- contentPaths: BlogContentPaths,
73
- context: LoadContext,
74
- options: PluginOptions,
75
- ): Promise<Feed | null> {
76
- if (!options.feedOptions) {
77
- throw new Error(
78
- 'Invalid options: "feedOptions" is not expected to be null.',
79
- );
80
- }
81
- const {siteConfig} = context;
82
- const blogPosts = await generateBlogPosts(contentPaths, context, options);
83
- if (!blogPosts.length) {
84
- return null;
85
- }
86
-
87
- const {feedOptions, routeBasePath} = options;
88
- const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
89
- const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
90
-
91
- const updated =
92
- (blogPosts[0] && blogPosts[0].metadata.date) ||
93
- new Date('2015-10-25T16:29:00.000-07:00');
94
-
95
- const feed = new Feed({
96
- id: blogBaseUrl,
97
- title: feedOptions.title || `${title} Blog`,
98
- updated,
99
- language: feedOptions.language,
100
- link: blogBaseUrl,
101
- description: feedOptions.description || `${siteConfig.title} Blog`,
102
- favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
103
- copyright: feedOptions.copyright,
104
- });
105
-
106
- blogPosts.forEach((post) => {
107
- const {
108
- id,
109
- metadata: {title: metadataTitle, permalink, date, description},
110
- } = post;
111
- feed.addItem({
112
- title: metadataTitle,
113
- id,
114
- link: normalizeUrl([siteUrl, permalink]),
115
- date,
116
- description,
117
- });
103
+ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
104
+ const result = await parseMarkdownFile(blogSourceAbsolute, {
105
+ removeContentTitle: true,
118
106
  });
119
-
120
- return feed;
107
+ return {
108
+ ...result,
109
+ frontMatter: validateBlogPostFrontMatter(result.frontMatter),
110
+ };
121
111
  }
122
112
 
123
- export async function generateBlogPosts(
113
+ const defaultReadingTime: ReadingTimeFunction = ({content, options}) =>
114
+ readingTime(content, options).minutes;
115
+
116
+ async function processBlogSourceFile(
117
+ blogSourceRelative: string,
124
118
  contentPaths: BlogContentPaths,
125
- {siteConfig, siteDir, i18n}: LoadContext,
119
+ context: LoadContext,
126
120
  options: PluginOptions,
127
- ): Promise<BlogPost[]> {
121
+ authorsMap?: AuthorsMap,
122
+ ): Promise<BlogPost | undefined> {
123
+ const {
124
+ siteConfig: {baseUrl},
125
+ siteDir,
126
+ i18n,
127
+ } = context;
128
128
  const {
129
- include,
130
129
  routeBasePath,
130
+ tagsBasePath: tagsRouteBasePath,
131
131
  truncateMarker,
132
132
  showReadingTime,
133
133
  editUrl,
134
134
  } = options;
135
135
 
136
- if (!fs.existsSync(contentPaths.contentPath)) {
137
- return [];
138
- }
139
-
140
- const {baseUrl = ''} = siteConfig;
141
- const blogSourceFiles = await globby(include, {
142
- cwd: contentPaths.contentPath,
143
- });
136
+ // Lookup in localized folder in priority
137
+ const blogDirPath = await getFolderContainingFile(
138
+ getContentPathList(contentPaths),
139
+ blogSourceRelative,
140
+ );
144
141
 
145
- const blogPosts: BlogPost[] = [];
142
+ const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
146
143
 
147
- async function processBlogSourceFile(blogSourceFile: string) {
148
- // Lookup in localized folder in priority
149
- const blogDirPath = await getFolderContainingFile(
150
- getContentPathList(contentPaths),
151
- blogSourceFile,
152
- );
144
+ const {frontMatter, content, contentTitle, excerpt} =
145
+ await parseBlogPostMarkdownFile(blogSourceAbsolute);
153
146
 
154
- const source = path.join(blogDirPath, blogSourceFile);
147
+ const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
155
148
 
156
- const {
157
- frontMatter: unsafeFrontMatter,
158
- content,
159
- contentTitle,
160
- excerpt,
161
- } = await parseMarkdownFile(source, {removeContentTitle: true});
162
- const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter);
149
+ if (frontMatter.draft && process.env.NODE_ENV === 'production') {
150
+ return undefined;
151
+ }
163
152
 
164
- const aliasedSource = aliasedSitePath(source, siteDir);
153
+ if (frontMatter.id) {
154
+ logger.warn`name=${'id'} header option is deprecated in path=${blogSourceRelative} file. Please use name=${'slug'} option instead.`;
155
+ }
165
156
 
166
- const blogFileName = path.basename(blogSourceFile);
157
+ const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
167
158
 
168
- if (frontMatter.draft && process.env.NODE_ENV === 'production') {
169
- return;
159
+ async function getDate(): Promise<Date> {
160
+ // Prefer user-defined date.
161
+ if (frontMatter.date) {
162
+ return new Date(frontMatter.date);
163
+ } else if (parsedBlogFileName.date) {
164
+ return parsedBlogFileName.date;
170
165
  }
166
+ // Fallback to file create time
167
+ return (await fs.stat(blogSourceAbsolute)).birthtime;
168
+ }
171
169
 
172
- if (frontMatter.id) {
173
- console.warn(
174
- chalk.yellow(
175
- `"id" header option is deprecated in ${blogFileName} file. Please use "slug" option instead.`,
176
- ),
177
- );
178
- }
170
+ const date = await getDate();
171
+ const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
179
172
 
180
- let date: Date | undefined;
181
- // Extract date and title from filename.
182
- const dateFilenameMatch = blogFileName.match(DATE_FILENAME_PATTERN);
183
- let linkName = blogFileName.replace(/\.mdx?$/, '');
173
+ const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
174
+ const description = frontMatter.description ?? excerpt ?? '';
184
175
 
185
- if (dateFilenameMatch) {
186
- const [, dateString, name] = dateFilenameMatch;
187
- // Always treat dates as UTC by adding the `Z`
188
- date = new Date(`${dateString}Z`);
189
- linkName = name;
190
- }
176
+ const slug = frontMatter.slug || parsedBlogFileName.slug;
191
177
 
192
- // Prefer user-defined date.
193
- if (frontMatter.date) {
194
- date = frontMatter.date;
195
- }
178
+ const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
196
179
 
197
- // Use file create time for blog.
198
- date = date ?? (await fs.stat(source)).birthtime;
199
- const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
200
-
201
- const title = frontMatter.title ?? contentTitle ?? linkName;
202
- const description = frontMatter.description ?? excerpt ?? '';
203
-
204
- const slug =
205
- frontMatter.slug ||
206
- (dateFilenameMatch ? toUrl({date, link: linkName}) : linkName);
207
-
208
- const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
209
-
210
- function getBlogEditUrl() {
211
- const blogPathRelative = path.relative(blogDirPath, path.resolve(source));
212
-
213
- if (typeof editUrl === 'function') {
214
- return editUrl({
215
- blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
216
- blogPath: posixPath(blogPathRelative),
217
- permalink,
218
- locale: i18n.currentLocale,
219
- });
220
- } else if (typeof editUrl === 'string') {
221
- const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
222
- const fileContentPath =
223
- isLocalized && options.editLocalizedFiles
224
- ? contentPaths.contentPathLocalized
225
- : contentPaths.contentPath;
226
-
227
- const contentPathEditUrl = normalizeUrl([
228
- editUrl,
229
- posixPath(path.relative(siteDir, fileContentPath)),
230
- ]);
231
-
232
- return getEditUrl(blogPathRelative, contentPathEditUrl);
233
- } else {
234
- return undefined;
235
- }
236
- }
180
+ function getBlogEditUrl() {
181
+ const blogPathRelative = path.relative(
182
+ blogDirPath,
183
+ path.resolve(blogSourceAbsolute),
184
+ );
237
185
 
238
- blogPosts.push({
239
- id: frontMatter.slug ?? title,
240
- metadata: {
186
+ if (typeof editUrl === 'function') {
187
+ return editUrl({
188
+ blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
189
+ blogPath: posixPath(blogPathRelative),
241
190
  permalink,
242
- editUrl: getBlogEditUrl(),
243
- source: aliasedSource,
244
- title,
245
- description,
246
- date,
247
- formattedDate,
248
- tags: frontMatter.tags ?? [],
249
- readingTime: showReadingTime ? readingTime(content).minutes : undefined,
250
- truncated: truncateMarker?.test(content) || false,
251
- },
252
- });
191
+ locale: i18n.currentLocale,
192
+ });
193
+ } else if (typeof editUrl === 'string') {
194
+ const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
195
+ const fileContentPath =
196
+ isLocalized && options.editLocalizedFiles
197
+ ? contentPaths.contentPathLocalized
198
+ : contentPaths.contentPath;
199
+
200
+ const contentPathEditUrl = normalizeUrl([
201
+ editUrl,
202
+ posixPath(path.relative(siteDir, fileContentPath)),
203
+ ]);
204
+
205
+ return getEditUrl(blogPathRelative, contentPathEditUrl);
206
+ }
207
+ return undefined;
253
208
  }
254
209
 
255
- await Promise.all(
256
- blogSourceFiles.map(async (blogSourceFile: string) => {
257
- try {
258
- return await processBlogSourceFile(blogSourceFile);
259
- } catch (e) {
260
- console.error(
261
- chalk.red(
262
- `Processing of blog source file failed for path "${blogSourceFile}"`,
263
- ),
264
- );
265
- throw e;
266
- }
267
- }),
268
- );
210
+ const tagsBasePath = normalizeUrl([
211
+ baseUrl,
212
+ routeBasePath,
213
+ tagsRouteBasePath,
214
+ ]);
215
+ const authors = getBlogPostAuthors({authorsMap, frontMatter});
216
+
217
+ return {
218
+ id: slug,
219
+ metadata: {
220
+ permalink,
221
+ editUrl: getBlogEditUrl(),
222
+ source: aliasedSource,
223
+ title,
224
+ description,
225
+ date,
226
+ formattedDate,
227
+ tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
228
+ readingTime: showReadingTime
229
+ ? options.readingTime({
230
+ content,
231
+ frontMatter,
232
+ defaultReadingTime,
233
+ })
234
+ : undefined,
235
+ truncated: truncateMarker?.test(content) || false,
236
+ authors,
237
+ },
238
+ content,
239
+ };
240
+ }
241
+
242
+ export async function generateBlogPosts(
243
+ contentPaths: BlogContentPaths,
244
+ context: LoadContext,
245
+ options: PluginOptions,
246
+ ): Promise<BlogPost[]> {
247
+ const {include, exclude} = options;
248
+
249
+ if (!fs.existsSync(contentPaths.contentPath)) {
250
+ return [];
251
+ }
252
+
253
+ const blogSourceFiles = await Globby(include, {
254
+ cwd: contentPaths.contentPath,
255
+ ignore: exclude,
256
+ });
257
+
258
+ const authorsMap = await getAuthorsMap({
259
+ contentPaths,
260
+ authorsMapPath: options.authorsMapPath,
261
+ });
262
+
263
+ const blogPosts = (
264
+ await Promise.all(
265
+ blogSourceFiles.map(async (blogSourceFile: string) => {
266
+ try {
267
+ return await processBlogSourceFile(
268
+ blogSourceFile,
269
+ contentPaths,
270
+ context,
271
+ options,
272
+ authorsMap,
273
+ );
274
+ } catch (e) {
275
+ logger.error`Processing of blog source file failed for path path=${blogSourceFile}.`;
276
+ throw e;
277
+ }
278
+ }),
279
+ )
280
+ ).filter(Boolean) as BlogPost[];
269
281
 
270
282
  blogPosts.sort(
271
283
  (a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(),
272
284
  );
273
285
 
286
+ if (options.sortPosts === 'ascending') {
287
+ return blogPosts.reverse();
288
+ }
274
289
  return blogPosts;
275
290
  }
276
291
 
File without changes