@docusaurus/plugin-content-blog 2.0.0-beta.677e53d4d → 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 (56) 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 +78 -77
  9. package/lib/markdownLoader.js +3 -3
  10. package/lib/pluginOptionSchema.d.ts +3 -26
  11. package/lib/pluginOptionSchema.js +22 -7
  12. package/lib/translations.d.ts +10 -0
  13. package/lib/translations.js +53 -0
  14. package/lib/types.d.ts +38 -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/_partials/somePartial.md +3 -0
  33. package/src/__tests__/__fixtures__/website/blog/_partials/subfolder/somePartial.md +3 -0
  34. package/src/__tests__/__fixtures__/website/blog/_somePartial.md +3 -0
  35. package/src/__tests__/__fixtures__/website/blog/authors.yml +4 -0
  36. package/src/__tests__/__fixtures__/website/blog/mdx-blog-post.mdx +36 -0
  37. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +4 -0
  38. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
  39. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/authors.yml +5 -0
  40. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +81 -3
  41. package/src/__tests__/__snapshots__/translations.test.ts.snap +64 -0
  42. package/src/__tests__/authors.test.ts +608 -0
  43. package/src/__tests__/blogFrontMatter.test.ts +93 -16
  44. package/src/__tests__/blogUtils.test.ts +94 -0
  45. package/src/__tests__/generateBlogFeed.test.ts +7 -1
  46. package/src/__tests__/index.test.ts +63 -12
  47. package/src/__tests__/pluginOptionSchema.test.ts +3 -3
  48. package/src/__tests__/translations.test.ts +92 -0
  49. package/src/authors.ts +202 -0
  50. package/src/blogFrontMatter.ts +73 -33
  51. package/src/blogUtils.ts +206 -131
  52. package/src/index.ts +98 -71
  53. package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
  54. package/src/pluginOptionSchema.ts +25 -9
  55. package/src/translations.ts +63 -0
  56. package/src/types.ts +48 -16
package/src/blogUtils.ts CHANGED
@@ -6,18 +6,18 @@
6
6
  */
7
7
 
8
8
  import fs from 'fs-extra';
9
- import globby from 'globby';
10
9
  import chalk from 'chalk';
11
10
  import path from 'path';
12
11
  import readingTime from 'reading-time';
13
- import {Feed} from 'feed';
14
- import {keyBy, mapValues} from 'lodash';
12
+ import {Feed, Author as FeedAuthor} from 'feed';
13
+ import {compact, keyBy, mapValues} from 'lodash';
15
14
  import {
16
15
  PluginOptions,
17
16
  BlogPost,
18
- DateLink,
19
17
  BlogContentPaths,
20
18
  BlogMarkdownLoaderOptions,
19
+ BlogTags,
20
+ Author,
21
21
  } from './types';
22
22
  import {
23
23
  parseMarkdownFile,
@@ -26,10 +26,15 @@ import {
26
26
  getEditUrl,
27
27
  getFolderContainingFile,
28
28
  posixPath,
29
+ mdxToHtml,
29
30
  replaceMarkdownLinks,
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,157 +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
190
  routeBasePath,
191
+ tagsBasePath: tagsRouteBasePath,
131
192
  truncateMarker,
132
193
  showReadingTime,
133
194
  editUrl,
134
195
  } = options;
135
196
 
136
- if (!fs.existsSync(contentPaths.contentPath)) {
137
- return [];
138
- }
197
+ // Lookup in localized folder in priority
198
+ const blogDirPath = await getFolderContainingFile(
199
+ getContentPathList(contentPaths),
200
+ blogSourceRelative,
201
+ );
139
202
 
140
- const {baseUrl = ''} = siteConfig;
141
- const blogSourceFiles = await globby(include, {
142
- cwd: contentPaths.contentPath,
143
- });
203
+ const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
204
+
205
+ const {frontMatter, content, contentTitle, excerpt} =
206
+ await parseBlogPostMarkdownFile(blogSourceAbsolute);
207
+
208
+ const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
144
209
 
145
- const blogPosts: BlogPost[] = [];
210
+ if (frontMatter.draft && process.env.NODE_ENV === 'production') {
211
+ return undefined;
212
+ }
146
213
 
147
- async function processBlogSourceFile(blogSourceFile: string) {
148
- // Lookup in localized folder in priority
149
- const blogDirPath = await getFolderContainingFile(
150
- getContentPathList(contentPaths),
151
- blogSourceFile,
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
+ ),
152
219
  );
220
+ }
153
221
 
154
- const source = path.join(blogDirPath, blogSourceFile);
222
+ const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
155
223
 
156
- const {
157
- frontMatter: unsafeFrontMatter,
158
- content,
159
- contentTitle,
160
- excerpt,
161
- } = await parseMarkdownFile(source, {removeContentTitle: true});
162
- 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
+ }
163
234
 
164
- const aliasedSource = aliasedSitePath(source, siteDir);
235
+ const date = await getDate();
236
+ const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
165
237
 
166
- const blogFileName = path.basename(blogSourceFile);
238
+ const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
239
+ const description = frontMatter.description ?? excerpt ?? '';
167
240
 
168
- if (frontMatter.draft && process.env.NODE_ENV === 'production') {
169
- return;
170
- }
241
+ const slug = frontMatter.slug || parsedBlogFileName.slug;
171
242
 
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
- }
243
+ const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
179
244
 
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?$/, '');
245
+ function getBlogEditUrl() {
246
+ const blogPathRelative = path.relative(
247
+ blogDirPath,
248
+ path.resolve(blogSourceAbsolute),
249
+ );
184
250
 
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;
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);
190
271
  }
272
+ return undefined;
273
+ }
191
274
 
192
- // Prefer user-defined date.
193
- if (frontMatter.date) {
194
- date = frontMatter.date;
195
- }
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
+ }
196
300
 
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
- }
301
+ export async function generateBlogPosts(
302
+ contentPaths: BlogContentPaths,
303
+ context: LoadContext,
304
+ options: PluginOptions,
305
+ ): Promise<BlogPost[]> {
306
+ const {include, exclude} = options;
237
307
 
238
- blogPosts.push({
239
- id: frontMatter.slug ?? title,
240
- metadata: {
241
- 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
- });
308
+ if (!fs.existsSync(contentPaths.contentPath)) {
309
+ return [];
253
310
  }
254
311
 
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
- }),
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
+ ),
268
343
  );
269
344
 
270
345
  blogPosts.sort(