Skip to content

Commit

Permalink
fix(v2): improve BlogPostFrontMatter schema validation (#4759)
Browse files Browse the repository at this point in the history
* fix(v2): improve BlogPostFrontMatter schema validation

* Edit doc

* Add deprecate warning message

* minor changes, disable warnings temporarily

* only disable warnings + fix frontmatter date type

Co-authored-by: Nam Hoang Le <nam.hoang.le@mgm-tp.com>
Co-authored-by: slorber <lorber.sebastien@gmail.com>
  • Loading branch information
3 people authored May 14, 2021
1 parent b33ee82 commit e092910
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,255 @@ import {
validateBlogPostFrontMatter,
} from '../blogFrontMatter';

function testField(params: {
fieldName: keyof BlogPostFrontMatter;
validFrontMatters: BlogPostFrontMatter[];
convertibleFrontMatter?: [
ConvertableFrontMatter: Record<string, unknown>,
ConvertedFrontMatter: BlogPostFrontMatter,
][];
invalidFrontMatters?: [
InvalidFrontMatter: Record<string, unknown>,
ErrorMessage: string,
][];
}) {
describe(`"${params.fieldName}" field`, () => {
test('accept valid values', () => {
params.validFrontMatters.forEach((frontMatter) => {
expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
});
});

test('convert valid values', () => {
params.convertibleFrontMatter?.forEach(
([convertibleFrontMatter, convertedFrontMatter]) => {
expect(validateBlogPostFrontMatter(convertibleFrontMatter)).toEqual(
convertedFrontMatter,
);
},
);
});

test('throw error for values', () => {
params.invalidFrontMatters?.forEach(([frontMatter, message]) => {
expect(() => validateBlogPostFrontMatter(frontMatter)).toThrow(message);
});
});
});
}

describe('validateBlogPostFrontMatter', () => {
test('accept empty object', () => {
const frontMatter = {};
expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
});

test('accept valid values', () => {
const frontMatter: BlogPostFrontMatter = {
id: 'blog',
title: 'title',
description: 'description',
date: 'date',
slug: 'slug',
draft: true,
tags: ['hello', {label: 'tagLabel', permalink: '/tagPermalink'}],
};
test('accept unknown field', () => {
const frontMatter = {abc: '1'};
expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
});

// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
test('accept empty title', () => {
const frontMatter: BlogPostFrontMatter = {title: ''};
expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
testField({
fieldName: 'description',
validFrontMatters: [
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
{description: ''},
{description: 'description'},
],
});

// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
test('accept empty description', () => {
const frontMatter: BlogPostFrontMatter = {description: ''};
expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
testField({
fieldName: 'title',
validFrontMatters: [
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
{title: ''},
{title: 'title'},
],
});

// See https://github.com/facebook/docusaurus/issues/4642
test('convert tags as numbers', () => {
const frontMatter: BlogPostFrontMatter = {
tags: [
// @ts-expect-error: number for test
42,
{
// @ts-expect-error: number for test
label: 84,
permalink: '/tagPermalink',
},
testField({
fieldName: 'id',
validFrontMatters: [{id: '123'}, {id: 'id'}],
invalidFrontMatters: [[{id: ''}, 'is not allowed to be empty']],
});

testField({
fieldName: 'author',
validFrontMatters: [{author: '123'}, {author: 'author'}],
invalidFrontMatters: [[{author: ''}, 'is not allowed to be empty']],
});

testField({
fieldName: 'authorTitle',
validFrontMatters: [{authorTitle: '123'}, {authorTitle: 'authorTitle'}],
invalidFrontMatters: [[{authorTitle: ''}, 'is not allowed to be empty']],
});

testField({
fieldName: 'author_title',
validFrontMatters: [{author_title: '123'}, {author_title: 'author_title'}],
invalidFrontMatters: [[{author_title: ''}, 'is not allowed to be empty']],
});

testField({
fieldName: 'authorURL',
validFrontMatters: [{authorURL: 'https://docusaurus.io'}],
invalidFrontMatters: [
[{authorURL: ''}, 'is not allowed to be empty'],
[{authorURL: '@site/api/author'}, 'must be a valid uri'],
[{authorURL: '../../api/author'}, 'must be a valid uri'],
],
});

testField({
fieldName: 'author_url',
validFrontMatters: [{author_url: 'https://docusaurus.io'}],
invalidFrontMatters: [
[{author_url: ''}, 'is not allowed to be empty'],
[{author_url: '@site/api/author'}, 'must be a valid uri'],
[{author_url: '../../api/author'}, 'must be a valid uri'],
],
});

testField({
fieldName: 'authorImageURL',
validFrontMatters: [
{authorImageURL: 'https://docusaurus.io/asset/image.png'},
],
invalidFrontMatters: [
[{authorImageURL: ''}, 'is not allowed to be empty'],
[{authorImageURL: '@site/api/asset/image.png'}, 'must be a valid uri'],
[{authorImageURL: '../../api/asset/image.png'}, 'must be a valid uri'],
],
});

testField({
fieldName: 'author_image_url',
validFrontMatters: [
{author_image_url: 'https://docusaurus.io/asset/image.png'},
],
invalidFrontMatters: [
[{author_image_url: ''}, 'is not allowed to be empty'],
[{author_image_url: '@site/api/asset/image.png'}, 'must be a valid uri'],
[{author_image_url: '../../api/asset/image.png'}, 'must be a valid uri'],
],
});

testField({
fieldName: 'slug',
validFrontMatters: [
{slug: 'blog/'},
{slug: '/blog'},
{slug: '/blog/'},
{slug: './blog'},
{slug: '../blog'},
{slug: '../../blog'},
{slug: '/api/plugins/@docusaurus/plugin-debug'},
{slug: '@site/api/asset/image.png'},
],
invalidFrontMatters: [[{slug: ''}, 'is not allowed to be empty']],
});

testField({
fieldName: 'image',
validFrontMatters: [
{image: 'blog/'},
{image: '/blog'},
{image: '/blog/'},
{image: './blog'},
{image: '../blog'},
{image: '../../blog'},
{image: '/api/plugins/@docusaurus/plugin-debug'},
{image: '@site/api/asset/image.png'},
],
invalidFrontMatters: [
[{image: ''}, 'is not allowed to be empty'],
[{image: 'https://docusaurus.io'}, 'must be a valid relative uri'],
[
{image: 'https://docusaurus.io/blog/image.png'},
'must be a valid relative uri',
],
};
expect(validateBlogPostFrontMatter(frontMatter)).toEqual({
tags: [
'42',
{
label: '84',
permalink: '/tagPermalink',
},
],
});

testField({
fieldName: 'tags',
validFrontMatters: [
{tags: []},
{tags: ['hello']},
{tags: ['hello', 'world']},
{tags: ['hello', 'world']},
{tags: ['hello', {label: 'tagLabel', permalink: '/tagPermalink'}]},
],
invalidFrontMatters: [
[{tags: ''}, 'must be an array'],
[{tags: ['']}, 'is not allowed to be empty'],
],
// See https://github.com/facebook/docusaurus/issues/4642
convertibleFrontMatter: [
[{tags: [42]}, {tags: ['42']}],
[
{tags: [{label: 84, permalink: '/tagPermalink'}]},
{tags: [{label: '84', permalink: '/tagPermalink'}]},
],
});
],
});

testField({
fieldName: 'keywords',
validFrontMatters: [
{keywords: ['hello']},
{keywords: ['hello', 'world']},
{keywords: ['hello', 'world']},
{keywords: ['hello']},
],
invalidFrontMatters: [
[{keywords: ''}, 'must be an array'],
[{keywords: ['']}, 'is not allowed to be empty'],
[{keywords: []}, 'does not contain 1 required value(s)'],
],
});

testField({
fieldName: 'draft',
validFrontMatters: [{draft: true}, {draft: false}],
convertibleFrontMatter: [
[{draft: 'true'}, {draft: true}],
[{draft: 'false'}, {draft: false}],
],
invalidFrontMatters: [
[{draft: 'yes'}, 'must be a boolean'],
[{draft: 'no'}, 'must be a boolean'],
],
});

testField({
fieldName: 'hide_table_of_contents',
validFrontMatters: [
{hide_table_of_contents: true},
{hide_table_of_contents: false},
],
convertibleFrontMatter: [
[{hide_table_of_contents: 'true'}, {hide_table_of_contents: true}],
[{hide_table_of_contents: 'false'}, {hide_table_of_contents: false}],
],
invalidFrontMatters: [
[{hide_table_of_contents: 'yes'}, 'must be a boolean'],
[{hide_table_of_contents: 'no'}, 'must be a boolean'],
],
});

testField({
fieldName: 'date',
validFrontMatters: [
// @ts-expect-error: number for test
{date: new Date('2020-01-01')},
{date: '2020-01-01'},
{date: '2020'},
],
invalidFrontMatters: [
[{date: 'abc'}, 'must be a valid date'],
[{date: ''}, 'must be a valid date'],
],
});
});
46 changes: 41 additions & 5 deletions packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,36 @@
* LICENSE file in the root directory of this source tree.
*/

/* eslint-disable camelcase */

import {
JoiFrontMatter as Joi, // Custom instance for frontmatter
validateFrontMatter,
} from '@docusaurus/utils-validation';
import {Tag} from './types';

// TODO complete this frontmatter + add unit tests
export type BlogPostFrontMatter = {
id?: string;
title?: string;
description?: string;
tags?: (string | Tag)[];
slug?: string;
draft?: boolean;
date?: string;
date?: Date;

author?: string;
author_title?: string;
author_url?: string;
author_image_url?: string;

image?: string;
keywords?: string[];
hide_table_of_contents?: boolean;

/** @deprecated */
authorTitle?: string;
authorURL?: string;
authorImageURL?: string;
};

// NOTE: we don't add any default value on purpose here
Expand All @@ -39,10 +54,31 @@ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
title: Joi.string().allow(''),
description: Joi.string().allow(''),
tags: Joi.array().items(BlogTagSchema),
slug: Joi.string(),
draft: Joi.boolean(),
date: Joi.string().allow(''), // TODO validate the date better!
}).unknown();
date: Joi.date().raw(),

author: Joi.string(),
author_title: Joi.string(),
author_url: Joi.string().uri(),
author_image_url: Joi.string().uri(),
slug: Joi.string(),
image: Joi.string().uri({relativeOnly: true}),
keywords: Joi.array().items(Joi.string().required()),
hide_table_of_contents: Joi.boolean(),

// TODO re-enable warnings later, our v1 blog posts use those older frontmatter fields
authorURL: Joi.string().uri(),
// .warning('deprecate.error', { alternative: '"author_url"'}),
authorTitle: Joi.string(),
// .warning('deprecate.error', { alternative: '"author_title"'}),
authorImageURL: Joi.string().uri(),
// .warning('deprecate.error', { alternative: '"author_image_url"'}),
})
.unknown()
.messages({
'deprecate.error':
'{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
});

export function validateBlogPostFrontMatter(
frontMatter: Record<string, unknown>,
Expand Down
Loading

0 comments on commit e092910

Please sign in to comment.