@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.
- package/lib/.tsbuildinfo +1 -1
- package/lib/authors.d.ts +23 -0
- package/lib/authors.js +150 -0
- package/lib/blogFrontMatter.d.ts +19 -6
- package/lib/blogFrontMatter.js +31 -19
- package/lib/blogUtils.d.ts +10 -2
- package/lib/blogUtils.js +146 -104
- package/lib/index.js +76 -70
- package/lib/markdownLoader.js +3 -3
- package/lib/pluginOptionSchema.d.ts +3 -27
- package/lib/pluginOptionSchema.js +19 -7
- package/lib/translations.d.ts +10 -0
- package/lib/translations.js +53 -0
- package/lib/types.d.ts +37 -14
- package/package.json +13 -11
- package/src/__tests__/__fixtures__/authorsMapFiles/authors.json +29 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authors.yml +27 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.json +5 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.yml +3 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.json +3 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.yml +2 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.json +8 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.yml +3 -0
- package/src/__tests__/__fixtures__/component/Typography.tsx +6 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathEmpty/empty +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson1/authors.json +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson2/authors.json +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathNestedYml/sub/folder/authors.yml +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml1/authors.yml +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml2/authors.yml +0 -0
- package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
- package/src/__tests__/__fixtures__/website/blog/authors.yml +4 -0
- package/src/__tests__/__fixtures__/website/blog/mdx-blog-post.mdx +36 -0
- package/src/__tests__/__fixtures__/website/blog/simple-slug.md +4 -0
- package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
- package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/authors.yml +5 -0
- package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +41 -3
- package/src/__tests__/__snapshots__/translations.test.ts.snap +64 -0
- package/src/__tests__/authors.test.ts +608 -0
- package/src/__tests__/blogFrontMatter.test.ts +93 -16
- package/src/__tests__/blogUtils.test.ts +94 -0
- package/src/__tests__/generateBlogFeed.test.ts +4 -0
- package/src/__tests__/index.test.ts +63 -12
- package/src/__tests__/pluginOptionSchema.test.ts +3 -3
- package/src/__tests__/translations.test.ts +92 -0
- package/src/authors.ts +202 -0
- package/src/blogFrontMatter.ts +73 -33
- package/src/blogUtils.ts +205 -132
- package/src/index.ts +92 -61
- package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
- package/src/pluginOptionSchema.ts +22 -9
- package/src/translations.ts +63 -0
- 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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
context: LoadContext,
|
|
126
181
|
options: PluginOptions,
|
|
127
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
197
|
+
// Lookup in localized folder in priority
|
|
198
|
+
const blogDirPath = await getFolderContainingFile(
|
|
199
|
+
getContentPathList(contentPaths),
|
|
200
|
+
blogSourceRelative,
|
|
201
|
+
);
|
|
140
202
|
|
|
141
|
-
const
|
|
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
|
|
205
|
+
const {frontMatter, content, contentTitle, excerpt} =
|
|
206
|
+
await parseBlogPostMarkdownFile(blogSourceAbsolute);
|
|
148
207
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
222
|
+
const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
|
|
157
223
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
235
|
+
const date = await getDate();
|
|
236
|
+
const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
|
|
167
237
|
|
|
168
|
-
|
|
238
|
+
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
|
|
239
|
+
const description = frontMatter.description ?? excerpt ?? '';
|
|
169
240
|
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
241
|
+
const slug = frontMatter.slug || parsedBlogFileName.slug;
|
|
173
242
|
|
|
174
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
245
|
+
function getBlogEditUrl() {
|
|
246
|
+
const blogPathRelative = path.relative(
|
|
247
|
+
blogDirPath,
|
|
248
|
+
path.resolve(blogSourceAbsolute),
|
|
249
|
+
);
|
|
186
250
|
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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(
|