@docusaurus/plugin-content-blog 2.0.0-beta.1ab8aa0af → 2.0.0-beta.2

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.
@@ -9,6 +9,7 @@ import {
9
9
  BlogPostFrontMatter,
10
10
  validateBlogPostFrontMatter,
11
11
  } from '../blogFrontMatter';
12
+ import escapeStringRegexp from 'escape-string-regexp';
12
13
 
13
14
  function testField(params: {
14
15
  fieldName: keyof BlogPostFrontMatter;
@@ -41,7 +42,20 @@ function testField(params: {
41
42
 
42
43
  test('throw error for values', () => {
43
44
  params.invalidFrontMatters?.forEach(([frontMatter, message]) => {
44
- expect(() => validateBlogPostFrontMatter(frontMatter)).toThrow(message);
45
+ try {
46
+ validateBlogPostFrontMatter(frontMatter);
47
+ fail(
48
+ new Error(
49
+ `Blog frontmatter is expected to be rejected, but was accepted successfully:\n ${JSON.stringify(
50
+ frontMatter,
51
+ null,
52
+ 2,
53
+ )}`,
54
+ ),
55
+ );
56
+ } catch (e) {
57
+ expect(e.message).toMatch(new RegExp(escapeStringRegexp(message)));
58
+ }
45
59
  });
46
60
  });
47
61
  });
@@ -57,7 +71,9 @@ describe('validateBlogPostFrontMatter', () => {
57
71
  const frontMatter = {abc: '1'};
58
72
  expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
59
73
  });
74
+ });
60
75
 
76
+ describe('validateBlogPostFrontMatter description', () => {
61
77
  testField({
62
78
  fieldName: 'description',
63
79
  validFrontMatters: [
@@ -66,7 +82,9 @@ describe('validateBlogPostFrontMatter', () => {
66
82
  {description: 'description'},
67
83
  ],
68
84
  });
85
+ });
69
86
 
87
+ describe('validateBlogPostFrontMatter title', () => {
70
88
  testField({
71
89
  fieldName: 'title',
72
90
  validFrontMatters: [
@@ -75,25 +93,25 @@ describe('validateBlogPostFrontMatter', () => {
75
93
  {title: 'title'},
76
94
  ],
77
95
  });
96
+ });
78
97
 
98
+ describe('validateBlogPostFrontMatter id', () => {
79
99
  testField({
80
100
  fieldName: 'id',
81
101
  validFrontMatters: [{id: '123'}, {id: 'id'}],
82
102
  invalidFrontMatters: [[{id: ''}, 'is not allowed to be empty']],
83
103
  });
104
+ });
84
105
 
106
+ describe('validateBlogPostFrontMatter author', () => {
85
107
  testField({
86
108
  fieldName: 'author',
87
109
  validFrontMatters: [{author: '123'}, {author: 'author'}],
88
110
  invalidFrontMatters: [[{author: ''}, 'is not allowed to be empty']],
89
111
  });
112
+ });
90
113
 
91
- testField({
92
- fieldName: 'authorTitle',
93
- validFrontMatters: [{authorTitle: '123'}, {authorTitle: 'authorTitle'}],
94
- invalidFrontMatters: [[{authorTitle: ''}, 'is not allowed to be empty']],
95
- });
96
-
114
+ describe('validateBlogPostFrontMatter author_title', () => {
97
115
  testField({
98
116
  fieldName: 'author_title',
99
117
  validFrontMatters: [{author_title: '123'}, {author_title: 'author_title'}],
@@ -101,49 +119,75 @@ describe('validateBlogPostFrontMatter', () => {
101
119
  });
102
120
 
103
121
  testField({
104
- fieldName: 'authorURL',
105
- validFrontMatters: [{authorURL: 'https://docusaurus.io'}],
106
- invalidFrontMatters: [
107
- [{authorURL: ''}, 'is not allowed to be empty'],
108
- [{authorURL: '@site/api/author'}, 'must be a valid uri'],
109
- [{authorURL: '../../api/author'}, 'must be a valid uri'],
110
- ],
122
+ fieldName: 'authorTitle',
123
+ validFrontMatters: [{authorTitle: '123'}, {authorTitle: 'authorTitle'}],
124
+ invalidFrontMatters: [[{authorTitle: ''}, 'is not allowed to be empty']],
111
125
  });
126
+ });
112
127
 
128
+ describe('validateBlogPostFrontMatter author_url', () => {
113
129
  testField({
114
130
  fieldName: 'author_url',
115
- validFrontMatters: [{author_url: 'https://docusaurus.io'}],
131
+ validFrontMatters: [
132
+ {author_url: 'https://docusaurus.io'},
133
+ {author_url: '../../relative'},
134
+ {author_url: '/absolute'},
135
+ ],
116
136
  invalidFrontMatters: [
117
- [{author_url: ''}, 'is not allowed to be empty'],
118
- [{author_url: '@site/api/author'}, 'must be a valid uri'],
119
- [{author_url: '../../api/author'}, 'must be a valid uri'],
137
+ [
138
+ {author_url: ''},
139
+ '"author_url" does not match any of the allowed types',
140
+ ],
120
141
  ],
121
142
  });
122
143
 
123
144
  testField({
124
- fieldName: 'authorImageURL',
145
+ fieldName: 'authorURL',
125
146
  validFrontMatters: [
126
- {authorImageURL: 'https://docusaurus.io/asset/image.png'},
147
+ {authorURL: 'https://docusaurus.io'},
148
+ {authorURL: '../../relative'},
149
+ {authorURL: '/absolute'},
127
150
  ],
151
+
128
152
  invalidFrontMatters: [
129
- [{authorImageURL: ''}, 'is not allowed to be empty'],
130
- [{authorImageURL: '@site/api/asset/image.png'}, 'must be a valid uri'],
131
- [{authorImageURL: '../../api/asset/image.png'}, 'must be a valid uri'],
153
+ [{authorURL: ''}, '"authorURL" does not match any of the allowed types'],
132
154
  ],
133
155
  });
156
+ });
134
157
 
158
+ describe('validateBlogPostFrontMatter author_image_url', () => {
135
159
  testField({
136
160
  fieldName: 'author_image_url',
137
161
  validFrontMatters: [
138
162
  {author_image_url: 'https://docusaurus.io/asset/image.png'},
163
+ {author_image_url: '../../relative'},
164
+ {author_image_url: '/absolute'},
139
165
  ],
140
166
  invalidFrontMatters: [
141
- [{author_image_url: ''}, 'is not allowed to be empty'],
142
- [{author_image_url: '@site/api/asset/image.png'}, 'must be a valid uri'],
143
- [{author_image_url: '../../api/asset/image.png'}, 'must be a valid uri'],
167
+ [
168
+ {author_image_url: ''},
169
+ '"author_image_url" does not match any of the allowed types',
170
+ ],
144
171
  ],
145
172
  });
146
173
 
174
+ testField({
175
+ fieldName: 'authorImageURL',
176
+ validFrontMatters: [
177
+ {authorImageURL: 'https://docusaurus.io/asset/image.png'},
178
+ {authorImageURL: '../../relative'},
179
+ {authorImageURL: '/absolute'},
180
+ ],
181
+ invalidFrontMatters: [
182
+ [
183
+ {authorImageURL: ''},
184
+ '"authorImageURL" does not match any of the allowed types',
185
+ ],
186
+ ],
187
+ });
188
+ });
189
+
190
+ describe('validateBlogPostFrontMatter slug', () => {
147
191
  testField({
148
192
  fieldName: 'slug',
149
193
  validFrontMatters: [
@@ -158,10 +202,13 @@ describe('validateBlogPostFrontMatter', () => {
158
202
  ],
159
203
  invalidFrontMatters: [[{slug: ''}, 'is not allowed to be empty']],
160
204
  });
205
+ });
161
206
 
207
+ describe('validateBlogPostFrontMatter image', () => {
162
208
  testField({
163
209
  fieldName: 'image',
164
210
  validFrontMatters: [
211
+ {image: 'https://docusaurus.io/image.png'},
165
212
  {image: 'blog/'},
166
213
  {image: '/blog'},
167
214
  {image: '/blog/'},
@@ -172,15 +219,12 @@ describe('validateBlogPostFrontMatter', () => {
172
219
  {image: '@site/api/asset/image.png'},
173
220
  ],
174
221
  invalidFrontMatters: [
175
- [{image: ''}, 'is not allowed to be empty'],
176
- [{image: 'https://docusaurus.io'}, 'must be a valid relative uri'],
177
- [
178
- {image: 'https://docusaurus.io/blog/image.png'},
179
- 'must be a valid relative uri',
180
- ],
222
+ [{image: ''}, '"image" does not match any of the allowed types'],
181
223
  ],
182
224
  });
225
+ });
183
226
 
227
+ describe('validateBlogPostFrontMatter tags', () => {
184
228
  testField({
185
229
  fieldName: 'tags',
186
230
  validFrontMatters: [
@@ -203,7 +247,9 @@ describe('validateBlogPostFrontMatter', () => {
203
247
  ],
204
248
  ],
205
249
  });
250
+ });
206
251
 
252
+ describe('validateBlogPostFrontMatter keywords', () => {
207
253
  testField({
208
254
  fieldName: 'keywords',
209
255
  validFrontMatters: [
@@ -218,7 +264,9 @@ describe('validateBlogPostFrontMatter', () => {
218
264
  [{keywords: []}, 'does not contain 1 required value(s)'],
219
265
  ],
220
266
  });
267
+ });
221
268
 
269
+ describe('validateBlogPostFrontMatter draft', () => {
222
270
  testField({
223
271
  fieldName: 'draft',
224
272
  validFrontMatters: [{draft: true}, {draft: false}],
@@ -231,7 +279,9 @@ describe('validateBlogPostFrontMatter', () => {
231
279
  [{draft: 'no'}, 'must be a boolean'],
232
280
  ],
233
281
  });
282
+ });
234
283
 
284
+ describe('validateBlogPostFrontMatter hide_table_of_contents', () => {
235
285
  testField({
236
286
  fieldName: 'hide_table_of_contents',
237
287
  validFrontMatters: [
@@ -247,7 +297,9 @@ describe('validateBlogPostFrontMatter', () => {
247
297
  [{hide_table_of_contents: 'no'}, 'must be a boolean'],
248
298
  ],
249
299
  });
300
+ });
250
301
 
302
+ describe('validateBlogPostFrontMatter date', () => {
251
303
  testField({
252
304
  fieldName: 'date',
253
305
  validFrontMatters: [
@@ -32,7 +32,7 @@ function getBlogContentPaths(siteDir: string): BlogContentPaths {
32
32
  describe('blogFeed', () => {
33
33
  (['atom', 'rss'] as const).forEach((feedType) => {
34
34
  describe(`${feedType}`, () => {
35
- test('can show feed without posts', async () => {
35
+ test('should not show feed without posts', async () => {
36
36
  const siteDir = __dirname;
37
37
  const siteConfig = {
38
38
  title: 'Hello',
@@ -68,7 +68,7 @@ describe('blogFeed', () => {
68
68
  const generatedFilesDir = path.resolve(siteDir, '.docusaurus');
69
69
  const siteConfig = {
70
70
  title: 'Hello',
71
- baseUrl: '/',
71
+ baseUrl: '/myBaseUrl/',
72
72
  url: 'https://docusaurus.io',
73
73
  favicon: 'image/favicon.ico',
74
74
  };
@@ -9,6 +9,7 @@
9
9
 
10
10
  import {
11
11
  JoiFrontMatter as Joi, // Custom instance for frontmatter
12
+ URISchema,
12
13
  validateFrontMatter,
13
14
  } from '@docusaurus/utils-validation';
14
15
  import {Tag} from './types';
@@ -59,19 +60,19 @@ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
59
60
 
60
61
  author: Joi.string(),
61
62
  author_title: Joi.string(),
62
- author_url: Joi.string().uri(),
63
- author_image_url: Joi.string().uri(),
63
+ author_url: URISchema,
64
+ author_image_url: URISchema,
64
65
  slug: Joi.string(),
65
- image: Joi.string().uri({relativeOnly: true}),
66
+ image: URISchema,
66
67
  keywords: Joi.array().items(Joi.string().required()),
67
68
  hide_table_of_contents: Joi.boolean(),
68
69
 
69
70
  // TODO re-enable warnings later, our v1 blog posts use those older frontmatter fields
70
- authorURL: Joi.string().uri(),
71
+ authorURL: URISchema,
71
72
  // .warning('deprecate.error', { alternative: '"author_url"'}),
72
73
  authorTitle: Joi.string(),
73
74
  // .warning('deprecate.error', { alternative: '"author_title"'}),
74
- authorImageURL: Joi.string().uri(),
75
+ authorImageURL: URISchema,
75
76
  // .warning('deprecate.error', { alternative: '"author_image_url"'}),
76
77
  })
77
78
  .unknown()
package/src/blogUtils.ts CHANGED
@@ -55,6 +55,19 @@ function toUrl({date, link}: DateLink) {
55
55
  .replace(/-/g, '/')}/${link}`;
56
56
  }
57
57
 
58
+ function formatBlogPostDate(locale: string, date: Date): string {
59
+ try {
60
+ return new Intl.DateTimeFormat(locale, {
61
+ day: 'numeric',
62
+ month: 'long',
63
+ year: 'numeric',
64
+ timeZone: 'UTC',
65
+ }).format(date);
66
+ } catch (e) {
67
+ throw new Error(`Can't format blog post date "${date}"`);
68
+ }
69
+ }
70
+
58
71
  export async function generateBlogFeed(
59
72
  contentPaths: BlogContentPaths,
60
73
  context: LoadContext,
@@ -62,18 +75,18 @@ export async function generateBlogFeed(
62
75
  ): Promise<Feed | null> {
63
76
  if (!options.feedOptions) {
64
77
  throw new Error(
65
- 'Invalid options - `feedOptions` is not expected to be null.',
78
+ 'Invalid options: "feedOptions" is not expected to be null.',
66
79
  );
67
80
  }
68
81
  const {siteConfig} = context;
69
82
  const blogPosts = await generateBlogPosts(contentPaths, context, options);
70
- if (blogPosts == null) {
83
+ if (!blogPosts.length) {
71
84
  return null;
72
85
  }
73
86
 
74
87
  const {feedOptions, routeBasePath} = options;
75
- const {url: siteUrl, title, favicon} = siteConfig;
76
- const blogBaseUrl = normalizeUrl([siteUrl, routeBasePath]);
88
+ const {url: siteUrl, baseUrl, title, favicon} = siteConfig;
89
+ const blogBaseUrl = normalizeUrl([siteUrl, baseUrl, routeBasePath]);
77
90
 
78
91
  const updated =
79
92
  (blogPosts[0] && blogPosts[0].metadata.date) ||
@@ -86,7 +99,7 @@ export async function generateBlogFeed(
86
99
  language: feedOptions.language,
87
100
  link: blogBaseUrl,
88
101
  description: feedOptions.description || `${siteConfig.title} Blog`,
89
- favicon: normalizeUrl([siteUrl, favicon]),
102
+ favicon: favicon ? normalizeUrl([siteUrl, baseUrl, favicon]) : undefined,
90
103
  copyright: feedOptions.copyright,
91
104
  });
92
105
 
@@ -131,121 +144,126 @@ export async function generateBlogPosts(
131
144
 
132
145
  const blogPosts: BlogPost[] = [];
133
146
 
134
- await Promise.all(
135
- blogSourceFiles.map(async (blogSourceFile: string) => {
136
- // Lookup in localized folder in priority
137
- const blogDirPath = await getFolderContainingFile(
138
- getContentPathList(contentPaths),
139
- blogSourceFile,
140
- );
147
+ async function processBlogSourceFile(blogSourceFile: string) {
148
+ // Lookup in localized folder in priority
149
+ const blogDirPath = await getFolderContainingFile(
150
+ getContentPathList(contentPaths),
151
+ blogSourceFile,
152
+ );
141
153
 
142
- const source = path.join(blogDirPath, blogSourceFile);
154
+ const source = path.join(blogDirPath, blogSourceFile);
143
155
 
144
- const {
145
- frontMatter: unsafeFrontMatter,
146
- content,
147
- contentTitle,
148
- excerpt,
149
- } = await parseMarkdownFile(source);
150
- const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter);
156
+ const {
157
+ frontMatter: unsafeFrontMatter,
158
+ content,
159
+ contentTitle,
160
+ excerpt,
161
+ } = await parseMarkdownFile(source, {removeContentTitle: true});
162
+ const frontMatter = validateBlogPostFrontMatter(unsafeFrontMatter);
163
+
164
+ const aliasedSource = aliasedSitePath(source, siteDir);
165
+
166
+ const blogFileName = path.basename(blogSourceFile);
167
+
168
+ if (frontMatter.draft && process.env.NODE_ENV === 'production') {
169
+ return;
170
+ }
171
+
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
+ }
151
179
 
152
- const aliasedSource = aliasedSitePath(source, siteDir);
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?$/, '');
153
184
 
154
- const blogFileName = path.basename(blogSourceFile);
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
+ }
155
191
 
156
- if (frontMatter.draft && process.env.NODE_ENV === 'production') {
157
- return;
158
- }
192
+ // Prefer user-defined date.
193
+ if (frontMatter.date) {
194
+ date = frontMatter.date;
195
+ }
159
196
 
160
- if (frontMatter.id) {
161
- console.warn(
162
- chalk.yellow(
163
- `${blogFileName} - 'id' header option is deprecated. Please use 'slug' option instead.`,
164
- ),
165
- );
166
- }
197
+ // Use file create time for blog.
198
+ date = date ?? (await fs.stat(source)).birthtime;
199
+ const formattedDate = formatBlogPostDate(i18n.currentLocale, date);
167
200
 
168
- let date: Date | undefined;
169
- // Extract date and title from filename.
170
- const dateFilenameMatch = blogFileName.match(DATE_FILENAME_PATTERN);
171
- let linkName = blogFileName.replace(/\.mdx?$/, '');
201
+ const title = frontMatter.title ?? contentTitle ?? linkName;
202
+ const description = frontMatter.description ?? excerpt ?? '';
172
203
 
173
- if (dateFilenameMatch) {
174
- const [, dateString, name] = dateFilenameMatch;
175
- date = new Date(dateString);
176
- linkName = name;
177
- }
204
+ const slug =
205
+ frontMatter.slug ||
206
+ (dateFilenameMatch ? toUrl({date, link: linkName}) : linkName);
178
207
 
179
- // Prefer user-defined date.
180
- if (frontMatter.date) {
181
- date = frontMatter.date;
182
- }
183
-
184
- // Use file create time for blog.
185
- date = date ?? (await fs.stat(source)).birthtime;
186
- const formattedDate = new Intl.DateTimeFormat(i18n.currentLocale, {
187
- day: 'numeric',
188
- month: 'long',
189
- year: 'numeric',
190
- }).format(date);
208
+ const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
191
209
 
192
- const title = frontMatter.title ?? contentTitle ?? linkName;
193
- const description = frontMatter.description ?? excerpt ?? '';
210
+ function getBlogEditUrl() {
211
+ const blogPathRelative = path.relative(blogDirPath, path.resolve(source));
194
212
 
195
- const slug =
196
- frontMatter.slug ||
197
- (dateFilenameMatch ? toUrl({date, link: linkName}) : linkName);
198
-
199
- const permalink = normalizeUrl([baseUrl, routeBasePath, slug]);
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
+ }
237
+
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
+ });
253
+ }
200
254
 
201
- function getBlogEditUrl() {
202
- const blogPathRelative = path.relative(
203
- blogDirPath,
204
- path.resolve(source),
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
+ ),
205
264
  );
206
-
207
- if (typeof editUrl === 'function') {
208
- return editUrl({
209
- blogDirPath: posixPath(path.relative(siteDir, blogDirPath)),
210
- blogPath: posixPath(blogPathRelative),
211
- permalink,
212
- locale: i18n.currentLocale,
213
- });
214
- } else if (typeof editUrl === 'string') {
215
- const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
216
- const fileContentPath =
217
- isLocalized && options.editLocalizedFiles
218
- ? contentPaths.contentPathLocalized
219
- : contentPaths.contentPath;
220
-
221
- const contentPathEditUrl = normalizeUrl([
222
- editUrl,
223
- posixPath(path.relative(siteDir, fileContentPath)),
224
- ]);
225
-
226
- return getEditUrl(blogPathRelative, contentPathEditUrl);
227
- } else {
228
- return undefined;
229
- }
265
+ throw e;
230
266
  }
231
-
232
- blogPosts.push({
233
- id: frontMatter.slug ?? title,
234
- metadata: {
235
- permalink,
236
- editUrl: getBlogEditUrl(),
237
- source: aliasedSource,
238
- title,
239
- description,
240
- date,
241
- formattedDate,
242
- tags: frontMatter.tags ?? [],
243
- readingTime: showReadingTime
244
- ? readingTime(content).minutes
245
- : undefined,
246
- truncated: truncateMarker?.test(content) || false,
247
- },
248
- });
249
267
  }),
250
268
  );
251
269