@docusaurus/plugin-content-blog 2.0.0-beta.1 → 2.0.0-beta.10
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 +147 -0
- package/lib/blogFrontMatter.d.ts +19 -6
- package/lib/blogFrontMatter.js +35 -23
- package/lib/blogUtils.d.ts +10 -4
- package/lib/blogUtils.js +141 -136
- package/lib/feed.d.ts +20 -0
- package/lib/feed.js +90 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +109 -112
- package/lib/markdownLoader.d.ts +3 -6
- package/lib/markdownLoader.js +5 -5
- package/lib/pluginOptionSchema.d.ts +3 -26
- package/lib/pluginOptionSchema.js +28 -7
- package/lib/translations.d.ts +10 -0
- package/lib/translations.js +53 -0
- package/lib/types.d.ts +54 -14
- package/package.json +19 -13
- 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/_partials/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/website/blog/_partials/subfolder/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/website/blog/_somePartial.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/mdx-require-blog-post.mdx +14 -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__/__fixtures__/website/static/img/docusaurus.png +0 -0
- package/src/__tests__/__snapshots__/feed.test.ts.snap +164 -0
- package/src/__tests__/__snapshots__/translations.test.ts.snap +64 -0
- package/src/__tests__/authors.test.ts +608 -0
- package/src/__tests__/blogFrontMatter.test.ts +165 -36
- package/src/__tests__/blogUtils.test.ts +94 -0
- package/src/__tests__/{generateBlogFeed.test.ts → feed.test.ts} +35 -9
- package/src/__tests__/index.test.ts +84 -12
- package/src/__tests__/pluginOptionSchema.test.ts +3 -3
- package/src/__tests__/translations.test.ts +92 -0
- package/src/authors.ts +198 -0
- package/src/blogFrontMatter.ts +76 -37
- package/src/blogUtils.ts +202 -179
- package/{types.d.ts → src/deps.d.ts} +0 -0
- package/src/feed.ts +129 -0
- package/src/index.ts +131 -112
- package/src/markdownLoader.ts +8 -12
- package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
- package/src/pluginOptionSchema.ts +31 -9
- package/src/translations.ts +63 -0
- package/src/types.ts +69 -16
- package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -76
package/src/blogUtils.ts
CHANGED
|
@@ -6,18 +6,17 @@
|
|
|
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
12
|
import {keyBy, mapValues} from 'lodash';
|
|
15
13
|
import {
|
|
16
14
|
PluginOptions,
|
|
17
15
|
BlogPost,
|
|
18
|
-
DateLink,
|
|
19
16
|
BlogContentPaths,
|
|
20
17
|
BlogMarkdownLoaderOptions,
|
|
18
|
+
BlogTags,
|
|
19
|
+
ReadingTimeFunction,
|
|
21
20
|
} from './types';
|
|
22
21
|
import {
|
|
23
22
|
parseMarkdownFile,
|
|
@@ -27,9 +26,13 @@ import {
|
|
|
27
26
|
getFolderContainingFile,
|
|
28
27
|
posixPath,
|
|
29
28
|
replaceMarkdownLinks,
|
|
29
|
+
Globby,
|
|
30
|
+
normalizeFrontMatterTags,
|
|
31
|
+
groupTaggedItems,
|
|
30
32
|
} from '@docusaurus/utils';
|
|
31
33
|
import {LoadContext} from '@docusaurus/types';
|
|
32
34
|
import {validateBlogPostFrontMatter} from './blogFrontMatter';
|
|
35
|
+
import {AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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,200 @@ function formatBlogPostDate(locale: string, date: Date): string {
|
|
|
68
100
|
}
|
|
69
101
|
}
|
|
70
102
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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: normalizeUrl([siteUrl, baseUrl, favicon]),
|
|
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
|
-
|
|
107
|
+
return {
|
|
108
|
+
...result,
|
|
109
|
+
frontMatter: validateBlogPostFrontMatter(result.frontMatter),
|
|
110
|
+
};
|
|
121
111
|
}
|
|
122
112
|
|
|
123
|
-
|
|
113
|
+
const defaultReadingTime: ReadingTimeFunction = ({content, options}) =>
|
|
114
|
+
readingTime(content, options).minutes;
|
|
115
|
+
|
|
116
|
+
async function processBlogSourceFile(
|
|
117
|
+
blogSourceRelative: string,
|
|
124
118
|
contentPaths: BlogContentPaths,
|
|
125
|
-
|
|
119
|
+
context: LoadContext,
|
|
126
120
|
options: PluginOptions,
|
|
127
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
// Lookup in localized folder in priority
|
|
137
|
+
const blogDirPath = await getFolderContainingFile(
|
|
138
|
+
getContentPathList(contentPaths),
|
|
139
|
+
blogSourceRelative,
|
|
140
|
+
);
|
|
139
141
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
|
|
143
|
+
|
|
144
|
+
const {frontMatter, content, contentTitle, excerpt} =
|
|
145
|
+
await parseBlogPostMarkdownFile(blogSourceAbsolute);
|
|
144
146
|
|
|
145
|
-
const
|
|
147
|
+
const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
|
|
146
148
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
if (frontMatter.draft && process.env.NODE_ENV === 'production') {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (frontMatter.id) {
|
|
154
|
+
console.warn(
|
|
155
|
+
chalk.yellow(
|
|
156
|
+
`"id" header option is deprecated in ${blogSourceRelative} file. Please use "slug" option instead.`,
|
|
157
|
+
),
|
|
152
158
|
);
|
|
159
|
+
}
|
|
153
160
|
|
|
154
|
-
|
|
161
|
+
const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
|
|
155
162
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
async function getDate(): Promise<Date> {
|
|
164
|
+
// Prefer user-defined date.
|
|
165
|
+
if (frontMatter.date) {
|
|
166
|
+
return new Date(frontMatter.date);
|
|
167
|
+
} else if (parsedBlogFileName.date) {
|
|
168
|
+
return parsedBlogFileName.date;
|
|
169
|
+
}
|
|
170
|
+
// Fallback to file create time
|
|
171
|
+
return (await fs.stat(blogSourceAbsolute)).birthtime;
|
|
172
|
+
}
|
|
163
173
|
|
|
164
|
-
|
|
174
|
+
const date = await getDate();
|
|
175
|
+
const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
|
|
165
176
|
|
|
166
|
-
|
|
177
|
+
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
|
|
178
|
+
const description = frontMatter.description ?? excerpt ?? '';
|
|
167
179
|
|
|
168
|
-
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
180
|
+
const slug = frontMatter.slug || parsedBlogFileName.slug;
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
console.warn(
|
|
174
|
-
chalk.yellow(
|
|
175
|
-
`"id" header option is deprecated in ${blogFileName} file. Please use "slug" option instead.`,
|
|
176
|
-
),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
182
|
+
const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
function getBlogEditUrl() {
|
|
185
|
+
const blogPathRelative = path.relative(
|
|
186
|
+
blogDirPath,
|
|
187
|
+
path.resolve(blogSourceAbsolute),
|
|
188
|
+
);
|
|
184
189
|
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
if (typeof editUrl === 'function') {
|
|
191
|
+
return editUrl({
|
|
192
|
+
blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
|
|
193
|
+
blogPath: posixPath(blogPathRelative),
|
|
194
|
+
permalink,
|
|
195
|
+
locale: i18n.currentLocale,
|
|
196
|
+
});
|
|
197
|
+
} else if (typeof editUrl === 'string') {
|
|
198
|
+
const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
|
|
199
|
+
const fileContentPath =
|
|
200
|
+
isLocalized && options.editLocalizedFiles
|
|
201
|
+
? contentPaths.contentPathLocalized
|
|
202
|
+
: contentPaths.contentPath;
|
|
203
|
+
|
|
204
|
+
const contentPathEditUrl = normalizeUrl([
|
|
205
|
+
editUrl,
|
|
206
|
+
posixPath(path.relative(siteDir, fileContentPath)),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
return getEditUrl(blogPathRelative, contentPathEditUrl);
|
|
190
210
|
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
214
|
+
const tagsBasePath = normalizeUrl([
|
|
215
|
+
baseUrl,
|
|
216
|
+
routeBasePath,
|
|
217
|
+
tagsRouteBasePath,
|
|
218
|
+
]);
|
|
219
|
+
const authors = getBlogPostAuthors({authorsMap, frontMatter});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
id: slug,
|
|
223
|
+
metadata: {
|
|
224
|
+
permalink,
|
|
225
|
+
editUrl: getBlogEditUrl(),
|
|
226
|
+
source: aliasedSource,
|
|
227
|
+
title,
|
|
228
|
+
description,
|
|
229
|
+
date,
|
|
230
|
+
formattedDate,
|
|
231
|
+
tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
|
|
232
|
+
readingTime: showReadingTime
|
|
233
|
+
? options.readingTime({
|
|
234
|
+
content,
|
|
235
|
+
frontMatter,
|
|
236
|
+
defaultReadingTime,
|
|
237
|
+
})
|
|
238
|
+
: undefined,
|
|
239
|
+
truncated: truncateMarker?.test(content) || false,
|
|
240
|
+
authors,
|
|
241
|
+
},
|
|
242
|
+
content,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
196
245
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
}
|
|
246
|
+
export async function generateBlogPosts(
|
|
247
|
+
contentPaths: BlogContentPaths,
|
|
248
|
+
context: LoadContext,
|
|
249
|
+
options: PluginOptions,
|
|
250
|
+
): Promise<BlogPost[]> {
|
|
251
|
+
const {include, exclude} = options;
|
|
237
252
|
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
});
|
|
253
|
+
if (!fs.existsSync(contentPaths.contentPath)) {
|
|
254
|
+
return [];
|
|
253
255
|
}
|
|
254
256
|
|
|
255
|
-
await
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
257
|
+
const blogSourceFiles = await Globby(include, {
|
|
258
|
+
cwd: contentPaths.contentPath,
|
|
259
|
+
ignore: exclude,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const authorsMap = await getAuthorsMap({
|
|
263
|
+
contentPaths,
|
|
264
|
+
authorsMapPath: options.authorsMapPath,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const blogPosts = (
|
|
268
|
+
await Promise.all(
|
|
269
|
+
blogSourceFiles.map(async (blogSourceFile: string) => {
|
|
270
|
+
try {
|
|
271
|
+
return await processBlogSourceFile(
|
|
272
|
+
blogSourceFile,
|
|
273
|
+
contentPaths,
|
|
274
|
+
context,
|
|
275
|
+
options,
|
|
276
|
+
authorsMap,
|
|
277
|
+
);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
console.error(
|
|
280
|
+
chalk.red(
|
|
281
|
+
`Processing of blog source file failed for path "${blogSourceFile}"`,
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
throw e;
|
|
285
|
+
}
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
).filter(Boolean) as BlogPost[];
|
|
269
289
|
|
|
270
290
|
blogPosts.sort(
|
|
271
291
|
(a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(),
|
|
272
292
|
);
|
|
273
293
|
|
|
294
|
+
if (options.sortPosts === 'ascending') {
|
|
295
|
+
return blogPosts.reverse();
|
|
296
|
+
}
|
|
274
297
|
return blogPosts;
|
|
275
298
|
}
|
|
276
299
|
|
|
File without changes
|
package/src/feed.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {Feed, Author as FeedAuthor} from 'feed';
|
|
9
|
+
import {PluginOptions, Author, BlogPost, FeedType} from './types';
|
|
10
|
+
import {normalizeUrl, mdxToHtml} from '@docusaurus/utils';
|
|
11
|
+
import {DocusaurusConfig} from '@docusaurus/types';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
|
|
15
|
+
// TODO this is temporary until we handle mdxToHtml better
|
|
16
|
+
// It's hard to convert reliably JSX/require calls to an html feed content
|
|
17
|
+
// See https://github.com/facebook/docusaurus/issues/5664
|
|
18
|
+
function mdxToFeedContent(mdxContent: string): string | undefined {
|
|
19
|
+
try {
|
|
20
|
+
return mdxToHtml(mdxContent);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// TODO will we need a plugin option to configure how to handle such an error
|
|
23
|
+
// Swallow the error on purpose for now, until we understand better the problem space
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function generateBlogFeed({
|
|
29
|
+
blogPosts,
|
|
30
|
+
options,
|
|
31
|
+
siteConfig,
|
|
32
|
+
}: {
|
|
33
|
+
blogPosts: BlogPost[];
|
|
34
|
+
options: PluginOptions;
|
|
35
|
+
siteConfig: DocusaurusConfig;
|
|
36
|
+
}): Promise<Feed | null> {
|
|
37
|
+
if (!blogPosts.length) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {feedOptions, routeBasePath} = options;
|
|
42
|
+
const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
|
|
43
|
+
const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
|
|
44
|
+
|
|
45
|
+
const updated =
|
|
46
|
+
(blogPosts[0] && blogPosts[0].metadata.date) ||
|
|
47
|
+
new Date('2015-10-25T16:29:00.000-07:00'); // weird legacy magic date
|
|
48
|
+
|
|
49
|
+
const feed = new Feed({
|
|
50
|
+
id: blogBaseUrl,
|
|
51
|
+
title: feedOptions.title || `${title} Blog`,
|
|
52
|
+
updated,
|
|
53
|
+
language: feedOptions.language,
|
|
54
|
+
link: blogBaseUrl,
|
|
55
|
+
description: feedOptions.description || `${siteConfig.title} Blog`,
|
|
56
|
+
favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
|
|
57
|
+
copyright: feedOptions.copyright,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function toFeedAuthor(author: Author): FeedAuthor {
|
|
61
|
+
// TODO ask author emails?
|
|
62
|
+
// RSS feed requires email to render authors
|
|
63
|
+
return {name: author.name, link: author.url};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
blogPosts.forEach((post) => {
|
|
67
|
+
const {
|
|
68
|
+
id,
|
|
69
|
+
metadata: {title: metadataTitle, permalink, date, description, authors},
|
|
70
|
+
} = post;
|
|
71
|
+
feed.addItem({
|
|
72
|
+
title: metadataTitle,
|
|
73
|
+
id,
|
|
74
|
+
link: normalizeUrl([siteUrl, permalink]),
|
|
75
|
+
date,
|
|
76
|
+
description,
|
|
77
|
+
content: mdxToFeedContent(post.content),
|
|
78
|
+
author: authors.map(toFeedAuthor),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return feed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function createBlogFeedFile({
|
|
86
|
+
feed,
|
|
87
|
+
feedType,
|
|
88
|
+
filePath,
|
|
89
|
+
}: {
|
|
90
|
+
feed: Feed;
|
|
91
|
+
feedType: FeedType;
|
|
92
|
+
filePath: string;
|
|
93
|
+
}) {
|
|
94
|
+
const feedContent = feedType === 'rss' ? feed.rss2() : feed.atom1();
|
|
95
|
+
try {
|
|
96
|
+
await fs.outputFile(filePath, feedContent);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new Error(`Generating ${feedType} feed failed: ${err}.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function createBlogFeedFiles({
|
|
103
|
+
blogPosts,
|
|
104
|
+
options,
|
|
105
|
+
siteConfig,
|
|
106
|
+
outDir,
|
|
107
|
+
}: {
|
|
108
|
+
blogPosts: BlogPost[];
|
|
109
|
+
options: PluginOptions;
|
|
110
|
+
siteConfig: DocusaurusConfig;
|
|
111
|
+
outDir: string;
|
|
112
|
+
}): Promise<void> {
|
|
113
|
+
const feed = await generateBlogFeed({blogPosts, options, siteConfig});
|
|
114
|
+
|
|
115
|
+
const feedTypes = options.feedOptions.type;
|
|
116
|
+
if (!feed || !feedTypes) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await Promise.all(
|
|
121
|
+
feedTypes.map(async (feedType) => {
|
|
122
|
+
await createBlogFeedFile({
|
|
123
|
+
feed,
|
|
124
|
+
feedType,
|
|
125
|
+
filePath: path.join(outDir, options.routeBasePath, `${feedType}.xml`),
|
|
126
|
+
});
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
}
|