@docusaurus/plugin-content-blog 2.0.0-beta.6f366f4b4 → 2.0.0-beta.7

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 (53) hide show
  1. package/lib/.tsbuildinfo +1 -1
  2. package/lib/authors.d.ts +23 -0
  3. package/lib/authors.js +150 -0
  4. package/lib/blogFrontMatter.d.ts +19 -6
  5. package/lib/blogFrontMatter.js +31 -19
  6. package/lib/blogUtils.d.ts +10 -2
  7. package/lib/blogUtils.js +146 -104
  8. package/lib/index.js +76 -70
  9. package/lib/markdownLoader.js +3 -3
  10. package/lib/pluginOptionSchema.d.ts +3 -27
  11. package/lib/pluginOptionSchema.js +19 -7
  12. package/lib/translations.d.ts +10 -0
  13. package/lib/translations.js +53 -0
  14. package/lib/types.d.ts +37 -14
  15. package/package.json +13 -11
  16. package/src/__tests__/__fixtures__/authorsMapFiles/authors.json +29 -0
  17. package/src/__tests__/__fixtures__/authorsMapFiles/authors.yml +27 -0
  18. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.json +5 -0
  19. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.yml +3 -0
  20. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.json +3 -0
  21. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.yml +2 -0
  22. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.json +8 -0
  23. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.yml +3 -0
  24. package/src/__tests__/__fixtures__/component/Typography.tsx +6 -0
  25. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathEmpty/empty +0 -0
  26. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson1/authors.json +0 -0
  27. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson2/authors.json +0 -0
  28. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathNestedYml/sub/folder/authors.yml +0 -0
  29. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml1/authors.yml +0 -0
  30. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml2/authors.yml +0 -0
  31. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
  32. package/src/__tests__/__fixtures__/website/blog/authors.yml +4 -0
  33. package/src/__tests__/__fixtures__/website/blog/mdx-blog-post.mdx +36 -0
  34. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +4 -0
  35. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
  36. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/authors.yml +5 -0
  37. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +41 -3
  38. package/src/__tests__/__snapshots__/translations.test.ts.snap +64 -0
  39. package/src/__tests__/authors.test.ts +608 -0
  40. package/src/__tests__/blogFrontMatter.test.ts +93 -16
  41. package/src/__tests__/blogUtils.test.ts +94 -0
  42. package/src/__tests__/generateBlogFeed.test.ts +4 -0
  43. package/src/__tests__/index.test.ts +63 -12
  44. package/src/__tests__/pluginOptionSchema.test.ts +3 -3
  45. package/src/__tests__/translations.test.ts +92 -0
  46. package/src/authors.ts +202 -0
  47. package/src/blogFrontMatter.ts +73 -33
  48. package/src/blogUtils.ts +205 -132
  49. package/src/index.ts +92 -61
  50. package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
  51. package/src/pluginOptionSchema.ts +22 -9
  52. package/src/translations.ts +63 -0
  53. package/src/types.ts +47 -16
package/src/blogUtils.ts CHANGED
@@ -9,14 +9,15 @@ import fs from 'fs-extra';
9
9
  import chalk from 'chalk';
10
10
  import path from 'path';
11
11
  import readingTime from 'reading-time';
12
- import {Feed} from 'feed';
13
- import {keyBy, mapValues} from 'lodash';
12
+ import {Feed, Author as FeedAuthor} from 'feed';
13
+ import {compact, keyBy, mapValues} from 'lodash';
14
14
  import {
15
15
  PluginOptions,
16
16
  BlogPost,
17
- DateLink,
18
17
  BlogContentPaths,
19
18
  BlogMarkdownLoaderOptions,
19
+ BlogTags,
20
+ Author,
20
21
  } from './types';
21
22
  import {
22
23
  parseMarkdownFile,
@@ -25,11 +26,15 @@ import {
25
26
  getEditUrl,
26
27
  getFolderContainingFile,
27
28
  posixPath,
29
+ mdxToHtml,
28
30
  replaceMarkdownLinks,
29
31
  Globby,
32
+ normalizeFrontMatterTags,
33
+ groupTaggedItems,
30
34
  } from '@docusaurus/utils';
31
35
  import {LoadContext} from '@docusaurus/types';
32
36
  import {validateBlogPostFrontMatter} from './blogFrontMatter';
37
+ import {AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
33
38
 
34
39
  export function truncate(fileString: string, truncateMarker: RegExp): string {
35
40
  return fileString.split(truncateMarker, 1).shift()!;
@@ -44,15 +49,46 @@ export function getSourceToPermalink(
44
49
  );
45
50
  }
46
51
 
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?$/;
52
+ export function getBlogTags(blogPosts: BlogPost[]): BlogTags {
53
+ const groups = groupTaggedItems(
54
+ blogPosts,
55
+ (blogPost) => blogPost.metadata.tags,
56
+ );
57
+ return mapValues(groups, (group) => {
58
+ return {
59
+ name: group.tag.label,
60
+ items: group.items.map((item) => item.id),
61
+ permalink: group.tag.permalink,
62
+ };
63
+ });
64
+ }
50
65
 
51
- function toUrl({date, link}: DateLink) {
52
- return `${date
53
- .toISOString()
54
- .substring(0, '2019-01-01'.length)
55
- .replace(/-/g, '/')}/${link}`;
66
+ const DATE_FILENAME_REGEX =
67
+ /^(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/;
68
+
69
+ type ParsedBlogFileName = {
70
+ date: Date | undefined;
71
+ text: string;
72
+ slug: string;
73
+ };
74
+
75
+ export function parseBlogFileName(
76
+ blogSourceRelative: string,
77
+ ): ParsedBlogFileName {
78
+ const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
79
+ if (dateFilenameMatch) {
80
+ const dateString = dateFilenameMatch.groups!.date!;
81
+ const text = dateFilenameMatch.groups!.text!;
82
+ // Always treat dates as UTC by adding the `Z`
83
+ const date = new Date(`${dateString}Z`);
84
+ const slugDate = dateString.replace(/-/g, '/');
85
+ const slug = `/${slugDate}/${text}`;
86
+ return {date, text, slug};
87
+ } else {
88
+ const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
89
+ const slug = `/${text}`;
90
+ return {date: undefined, text, slug};
91
+ }
56
92
  }
57
93
 
58
94
  function formatBlogPostDate(locale: string, date: Date): string {
@@ -103,10 +139,16 @@ export async function generateBlogFeed(
103
139
  copyright: feedOptions.copyright,
104
140
  });
105
141
 
142
+ function toFeedAuthor(author: Author): FeedAuthor {
143
+ // TODO ask author emails?
144
+ // RSS feed requires email to render authors
145
+ return {name: author.name, link: author.url};
146
+ }
147
+
106
148
  blogPosts.forEach((post) => {
107
149
  const {
108
150
  id,
109
- metadata: {title: metadataTitle, permalink, date, description},
151
+ metadata: {title: metadataTitle, permalink, date, description, authors},
110
152
  } = post;
111
153
  feed.addItem({
112
154
  title: metadataTitle,
@@ -114,159 +156,190 @@ export async function generateBlogFeed(
114
156
  link: normalizeUrl([siteUrl, permalink]),
115
157
  date,
116
158
  description,
159
+ content: mdxToHtml(post.content),
160
+ author: authors.map(toFeedAuthor),
117
161
  });
118
162
  });
119
163
 
120
164
  return feed;
121
165
  }
122
166
 
123
- export async function generateBlogPosts(
167
+ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
168
+ const result = await parseMarkdownFile(blogSourceAbsolute, {
169
+ removeContentTitle: true,
170
+ });
171
+ return {
172
+ ...result,
173
+ frontMatter: validateBlogPostFrontMatter(result.frontMatter),
174
+ };
175
+ }
176
+
177
+ async function processBlogSourceFile(
178
+ blogSourceRelative: string,
124
179
  contentPaths: BlogContentPaths,
125
- {siteConfig, siteDir, i18n}: LoadContext,
180
+ context: LoadContext,
126
181
  options: PluginOptions,
127
- ): Promise<BlogPost[]> {
182
+ authorsMap?: AuthorsMap,
183
+ ): Promise<BlogPost | undefined> {
184
+ const {
185
+ siteConfig: {baseUrl},
186
+ siteDir,
187
+ i18n,
188
+ } = context;
128
189
  const {
129
- include,
130
- exclude,
131
190
  routeBasePath,
191
+ tagsBasePath: tagsRouteBasePath,
132
192
  truncateMarker,
133
193
  showReadingTime,
134
194
  editUrl,
135
195
  } = options;
136
196
 
137
- if (!fs.existsSync(contentPaths.contentPath)) {
138
- return [];
139
- }
197
+ // Lookup in localized folder in priority
198
+ const blogDirPath = await getFolderContainingFile(
199
+ getContentPathList(contentPaths),
200
+ blogSourceRelative,
201
+ );
140
202
 
141
- const {baseUrl = ''} = siteConfig;
142
- const blogSourceFiles = await Globby(include, {
143
- cwd: contentPaths.contentPath,
144
- ignore: exclude,
145
- });
203
+ const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
146
204
 
147
- const blogPosts: BlogPost[] = [];
205
+ const {frontMatter, content, contentTitle, excerpt} =
206
+ await parseBlogPostMarkdownFile(blogSourceAbsolute);
148
207
 
149
- async function processBlogSourceFile(blogSourceFile: string) {
150
- // Lookup in localized folder in priority
151
- const blogDirPath = await getFolderContainingFile(
152
- getContentPathList(contentPaths),
153
- blogSourceFile,
208
+ const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
209
+
210
+ if (frontMatter.draft && process.env.NODE_ENV === 'production') {
211
+ return undefined;
212
+ }
213
+
214
+ if (frontMatter.id) {
215
+ console.warn(
216
+ chalk.yellow(
217
+ `"id" header option is deprecated in ${blogSourceRelative} file. Please use "slug" option instead.`,
218
+ ),
154
219
  );
220
+ }
155
221
 
156
- const source = path.join(blogDirPath, blogSourceFile);
222
+ const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
157
223
 
158
- const {
159
- frontMatter: unsafeFrontMatter,
160
- content,
161
- contentTitle,
162
- excerpt,
163
- } = await parseMarkdownFile(source, {removeContentTitle: true});
164
- const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter);
224
+ async function getDate(): Promise<Date> {
225
+ // Prefer user-defined date.
226
+ if (frontMatter.date) {
227
+ return new Date(frontMatter.date);
228
+ } else if (parsedBlogFileName.date) {
229
+ return parsedBlogFileName.date;
230
+ }
231
+ // Fallback to file create time
232
+ return (await fs.stat(blogSourceAbsolute)).birthtime;
233
+ }
165
234
 
166
- const aliasedSource = aliasedSitePath(source, siteDir);
235
+ const date = await getDate();
236
+ const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
167
237
 
168
- const blogFileName = path.basename(blogSourceFile);
238
+ const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
239
+ const description = frontMatter.description ?? excerpt ?? '';
169
240
 
170
- if (frontMatter.draft && process.env.NODE_ENV === 'production') {
171
- return;
172
- }
241
+ const slug = frontMatter.slug || parsedBlogFileName.slug;
173
242
 
174
- if (frontMatter.id) {
175
- console.warn(
176
- chalk.yellow(
177
- `"id" header option is deprecated in ${blogFileName} file. Please use "slug" option instead.`,
178
- ),
179
- );
180
- }
243
+ const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
181
244
 
182
- let date: Date | undefined;
183
- // Extract date and title from filename.
184
- const dateFilenameMatch = blogFileName.match(DATE_FILENAME_PATTERN);
185
- let linkName = blogFileName.replace(/\.mdx?$/, '');
245
+ function getBlogEditUrl() {
246
+ const blogPathRelative = path.relative(
247
+ blogDirPath,
248
+ path.resolve(blogSourceAbsolute),
249
+ );
186
250
 
187
- if (dateFilenameMatch) {
188
- const [, dateString, name] = dateFilenameMatch;
189
- // Always treat dates as UTC by adding the `Z`
190
- date = new Date(`${dateString}Z`);
191
- linkName = name;
251
+ if (typeof editUrl === 'function') {
252
+ return editUrl({
253
+ blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
254
+ blogPath: posixPath(blogPathRelative),
255
+ permalink,
256
+ locale: i18n.currentLocale,
257
+ });
258
+ } else if (typeof editUrl === 'string') {
259
+ const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
260
+ const fileContentPath =
261
+ isLocalized && options.editLocalizedFiles
262
+ ? contentPaths.contentPathLocalized
263
+ : contentPaths.contentPath;
264
+
265
+ const contentPathEditUrl = normalizeUrl([
266
+ editUrl,
267
+ posixPath(path.relative(siteDir, fileContentPath)),
268
+ ]);
269
+
270
+ return getEditUrl(blogPathRelative, contentPathEditUrl);
192
271
  }
272
+ return undefined;
273
+ }
193
274
 
194
- // Prefer user-defined date.
195
- if (frontMatter.date) {
196
- date = frontMatter.date;
197
- }
275
+ const tagsBasePath = normalizeUrl([
276
+ baseUrl,
277
+ routeBasePath,
278
+ tagsRouteBasePath,
279
+ ]);
280
+ const authors = getBlogPostAuthors({authorsMap, frontMatter});
281
+
282
+ return {
283
+ id: frontMatter.slug ?? title,
284
+ metadata: {
285
+ permalink,
286
+ editUrl: getBlogEditUrl(),
287
+ source: aliasedSource,
288
+ title,
289
+ description,
290
+ date,
291
+ formattedDate,
292
+ tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
293
+ readingTime: showReadingTime ? readingTime(content).minutes : undefined,
294
+ truncated: truncateMarker?.test(content) || false,
295
+ authors,
296
+ },
297
+ content,
298
+ };
299
+ }
198
300
 
199
- // Use file create time for blog.
200
- date = date ?? (await fs.stat(source)).birthtime;
201
- const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
202
-
203
- const title = frontMatter.title ?? contentTitle ?? linkName;
204
- const description = frontMatter.description ?? excerpt ?? '';
205
-
206
- const slug =
207
- frontMatter.slug ||
208
- (dateFilenameMatch ? toUrl({date, link: linkName}) : linkName);
209
-
210
- const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
211
-
212
- function getBlogEditUrl() {
213
- const blogPathRelative = path.relative(blogDirPath, path.resolve(source));
214
-
215
- if (typeof editUrl === 'function') {
216
- return editUrl({
217
- blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
218
- blogPath: posixPath(blogPathRelative),
219
- permalink,
220
- locale: i18n.currentLocale,
221
- });
222
- } else if (typeof editUrl === 'string') {
223
- const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
224
- const fileContentPath =
225
- isLocalized && options.editLocalizedFiles
226
- ? contentPaths.contentPathLocalized
227
- : contentPaths.contentPath;
228
-
229
- const contentPathEditUrl = normalizeUrl([
230
- editUrl,
231
- posixPath(path.relative(siteDir, fileContentPath)),
232
- ]);
233
-
234
- return getEditUrl(blogPathRelative, contentPathEditUrl);
235
- } else {
236
- return undefined;
237
- }
238
- }
301
+ export async function generateBlogPosts(
302
+ contentPaths: BlogContentPaths,
303
+ context: LoadContext,
304
+ options: PluginOptions,
305
+ ): Promise<BlogPost[]> {
306
+ const {include, exclude} = options;
239
307
 
240
- blogPosts.push({
241
- id: frontMatter.slug ?? title,
242
- metadata: {
243
- permalink,
244
- editUrl: getBlogEditUrl(),
245
- source: aliasedSource,
246
- title,
247
- description,
248
- date,
249
- formattedDate,
250
- tags: frontMatter.tags ?? [],
251
- readingTime: showReadingTime ? readingTime(content).minutes : undefined,
252
- truncated: truncateMarker?.test(content) || false,
253
- },
254
- });
308
+ if (!fs.existsSync(contentPaths.contentPath)) {
309
+ return [];
255
310
  }
256
311
 
257
- await Promise.all(
258
- blogSourceFiles.map(async (blogSourceFile: string) => {
259
- try {
260
- return await processBlogSourceFile(blogSourceFile);
261
- } catch (e) {
262
- console.error(
263
- chalk.red(
264
- `Processing of blog source file failed for path "${blogSourceFile}"`,
265
- ),
266
- );
267
- throw e;
268
- }
269
- }),
312
+ const blogSourceFiles = await Globby(include, {
313
+ cwd: contentPaths.contentPath,
314
+ ignore: exclude,
315
+ });
316
+
317
+ const authorsMap = await getAuthorsMap({
318
+ contentPaths,
319
+ authorsMapPath: options.authorsMapPath,
320
+ });
321
+
322
+ const blogPosts: BlogPost[] = compact(
323
+ await Promise.all(
324
+ blogSourceFiles.map(async (blogSourceFile: string) => {
325
+ try {
326
+ return await processBlogSourceFile(
327
+ blogSourceFile,
328
+ contentPaths,
329
+ context,
330
+ options,
331
+ authorsMap,
332
+ );
333
+ } catch (e) {
334
+ console.error(
335
+ chalk.red(
336
+ `Processing of blog source file failed for path "${blogSourceFile}"`,
337
+ ),
338
+ );
339
+ throw e;
340
+ }
341
+ }),
342
+ ),
270
343
  );
271
344
 
272
345
  blogPosts.sort(