mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 10:17:55 +02:00
feat(blog): warn duplicate and inline authors (#10224)
Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com> Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
parent
1405b25fc7
commit
de59621fbb
16 changed files with 299 additions and 21 deletions
|
@ -5,7 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
type AuthorsMap,
|
||||
getAuthorsMap,
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {jest} from '@jest/globals';
|
||||
import {reportDuplicateAuthors, reportInlineAuthors} from '../authorsProblems';
|
||||
import type {Author} from '@docusaurus/plugin-content-blog';
|
||||
|
||||
const blogSourceRelative = 'doc.md';
|
||||
|
||||
describe('duplicate authors', () => {
|
||||
function testReport({authors}: {authors: Author[]}) {
|
||||
reportDuplicateAuthors({
|
||||
authors,
|
||||
blogSourceRelative,
|
||||
});
|
||||
}
|
||||
|
||||
it('allows duplicated inline authors', () => {
|
||||
const authors: Author[] = [
|
||||
{
|
||||
name: 'Sébastien Lorber',
|
||||
},
|
||||
{
|
||||
name: 'Sébastien Lorber',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
testReport({
|
||||
authors,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects duplicated key authors', () => {
|
||||
const authors: Author[] = [
|
||||
{
|
||||
key: 'slorber',
|
||||
name: 'Sébastien Lorber 1',
|
||||
title: 'some title',
|
||||
},
|
||||
{
|
||||
key: 'slorber',
|
||||
name: 'Sébastien Lorber 2',
|
||||
imageURL: '/slorber.png',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
testReport({
|
||||
authors,
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"Duplicate blog post authors were found in blog post "doc.md" front matter:
|
||||
- {"key":"slorber","name":"Sébastien Lorber 2","imageURL":"/slorber.png"}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inline authors', () => {
|
||||
type Options = Parameters<typeof reportInlineAuthors>[0]['options'];
|
||||
|
||||
function testReport({
|
||||
authors,
|
||||
options = {},
|
||||
}: {
|
||||
authors: Author[];
|
||||
options?: Partial<Options>;
|
||||
}) {
|
||||
const defaultOptions: Options = {
|
||||
onInlineAuthors: 'throw',
|
||||
authorsMapPath: 'authors.yml',
|
||||
};
|
||||
|
||||
reportInlineAuthors({
|
||||
authors,
|
||||
options: {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
},
|
||||
blogSourceRelative,
|
||||
});
|
||||
}
|
||||
|
||||
it('allows predefined authors', () => {
|
||||
const authors: Author[] = [
|
||||
{
|
||||
key: 'slorber',
|
||||
name: 'Sébastien Lorber',
|
||||
},
|
||||
{
|
||||
key: 'ozaki',
|
||||
name: 'Clément Couriol',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
testReport({
|
||||
authors,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects inline authors', () => {
|
||||
const authors: Author[] = [
|
||||
{
|
||||
key: 'slorber',
|
||||
name: 'Sébastien Lorber',
|
||||
},
|
||||
{name: 'Inline author 1'},
|
||||
{
|
||||
key: 'ozaki',
|
||||
name: 'Clément Couriol',
|
||||
},
|
||||
{imageURL: '/inline-author2.png'},
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
testReport({
|
||||
authors,
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"Some blog authors used in "doc.md" are not defined in "authors.yml":
|
||||
- {"name":"Inline author 1"}
|
||||
- {"imageURL":"/inline-author2.png"}
|
||||
|
||||
Note that we recommend to declare authors once in a "authors.yml" file and reference them by key in blog posts front matter to avoid author info duplication.
|
||||
But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
|
||||
More info at https://docusaurus.io/docs/blog
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('warn inline authors', () => {
|
||||
const authors: Author[] = [
|
||||
{
|
||||
key: 'slorber',
|
||||
name: 'Sébastien Lorber',
|
||||
},
|
||||
{name: 'Inline author 1'},
|
||||
{
|
||||
key: 'ozaki',
|
||||
name: 'Clément Couriol',
|
||||
},
|
||||
{imageURL: '/inline-author2.png'},
|
||||
];
|
||||
|
||||
const consoleMock = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
expect(() =>
|
||||
testReport({
|
||||
authors,
|
||||
options: {
|
||||
onInlineAuthors: 'warn',
|
||||
},
|
||||
}),
|
||||
).not.toThrow();
|
||||
expect(consoleMock).toHaveBeenCalledTimes(1);
|
||||
expect(consoleMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
[
|
||||
"[WARNING] Some blog authors used in "doc.md" are not defined in "authors.yml":
|
||||
- {"name":"Inline author 1"}
|
||||
- {"imageURL":"/inline-author2.png"}
|
||||
|
||||
Note that we recommend to declare authors once in a "authors.yml" file and reference them by key in blog posts front matter to avoid author info duplication.
|
||||
But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
|
||||
More info at https://docusaurus.io/docs/blog
|
||||
",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -101,6 +101,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
|||
defaultReadingTime({content}),
|
||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||
onInlineTags: 'ignore',
|
||||
onInlineAuthors: 'ignore',
|
||||
} as PluginOptions,
|
||||
);
|
||||
|
||||
|
@ -143,6 +144,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
|||
defaultReadingTime({content}),
|
||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||
onInlineTags: 'ignore',
|
||||
onInlineAuthors: 'ignore',
|
||||
} as PluginOptions,
|
||||
);
|
||||
|
||||
|
@ -197,6 +199,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
|||
defaultReadingTime({content}),
|
||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||
onInlineTags: 'ignore',
|
||||
onInlineAuthors: 'ignore',
|
||||
} as PluginOptions,
|
||||
);
|
||||
|
||||
|
@ -242,6 +245,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
|||
defaultReadingTime({content}),
|
||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||
onInlineTags: 'ignore',
|
||||
onInlineAuthors: 'ignore',
|
||||
} as PluginOptions,
|
||||
);
|
||||
|
||||
|
@ -287,6 +291,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
|
|||
defaultReadingTime({content}),
|
||||
truncateMarker: /<!--\s*truncate\s*-->/,
|
||||
onInlineTags: 'ignore',
|
||||
onInlineAuthors: 'ignore',
|
||||
} as PluginOptions,
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import logger from '@docusaurus/logger';
|
||||
import type {Author, PluginOptions} from '@docusaurus/plugin-content-blog';
|
||||
|
||||
export function reportAuthorsProblems(params: {
|
||||
authors: Author[];
|
||||
blogSourceRelative: string;
|
||||
options: Pick<PluginOptions, 'onInlineAuthors' | 'authorsMapPath'>;
|
||||
}): void {
|
||||
reportInlineAuthors(params);
|
||||
reportDuplicateAuthors(params);
|
||||
}
|
||||
|
||||
export function reportInlineAuthors({
|
||||
authors,
|
||||
blogSourceRelative,
|
||||
options: {onInlineAuthors, authorsMapPath},
|
||||
}: {
|
||||
authors: Author[];
|
||||
blogSourceRelative: string;
|
||||
options: Pick<PluginOptions, 'onInlineAuthors' | 'authorsMapPath'>;
|
||||
}): void {
|
||||
if (onInlineAuthors === 'ignore') {
|
||||
return;
|
||||
}
|
||||
const inlineAuthors = authors.filter((author) => !author.key);
|
||||
if (inlineAuthors.length > 0) {
|
||||
logger.report(onInlineAuthors)(
|
||||
logger.interpolate`Some blog authors used in path=${blogSourceRelative} are not defined in path=${authorsMapPath}:
|
||||
- ${inlineAuthors.map(authorToString).join('\n- ')}
|
||||
|
||||
Note that we recommend to declare authors once in a path=${authorsMapPath} file and reference them by key in blog posts front matter to avoid author info duplication.
|
||||
But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
|
||||
More info at url=${'https://docusaurus.io/docs/blog'}
|
||||
`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function reportDuplicateAuthors({
|
||||
authors,
|
||||
blogSourceRelative,
|
||||
}: {
|
||||
authors: Author[];
|
||||
blogSourceRelative: string;
|
||||
}): void {
|
||||
const duplicateAuthors = _(authors)
|
||||
// for now we only check for predefined authors duplicates
|
||||
.filter((author) => !!author.key)
|
||||
.groupBy((author) => author.key)
|
||||
.pickBy((authorsByKey) => authorsByKey.length > 1)
|
||||
// We only keep the "test" of all the duplicate groups
|
||||
// The first author of a group is not really a duplicate...
|
||||
.flatMap(([, ...rest]) => rest)
|
||||
.value();
|
||||
|
||||
if (duplicateAuthors.length > 0) {
|
||||
throw new Error(logger.interpolate`Duplicate blog post authors were found in blog post path=${blogSourceRelative} front matter:
|
||||
- ${duplicateAuthors.map(authorToString).join('\n- ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function authorToString(author: Author) {
|
||||
return JSON.stringify(author);
|
||||
}
|
|
@ -30,6 +30,7 @@ import {
|
|||
import {getTagsFile} from '@docusaurus/utils-validation';
|
||||
import {validateBlogPostFrontMatter} from './frontMatter';
|
||||
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
|
||||
import {reportAuthorsProblems} from './authorsProblems';
|
||||
import type {TagsFile} from '@docusaurus/utils';
|
||||
import type {LoadContext, ParseFrontMatter} from '@docusaurus/types';
|
||||
import type {
|
||||
|
@ -317,7 +318,13 @@ async function processBlogSourceFile(
|
|||
routeBasePath,
|
||||
tagsRouteBasePath,
|
||||
]);
|
||||
|
||||
const authors = getBlogPostAuthors({authorsMap, frontMatter, baseUrl});
|
||||
reportAuthorsProblems({
|
||||
authors,
|
||||
blogSourceRelative,
|
||||
options,
|
||||
});
|
||||
|
||||
const tags = normalizeTags({
|
||||
options,
|
||||
|
|
|
@ -58,6 +58,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
|||
processBlogPosts: async () => undefined,
|
||||
onInlineTags: 'warn',
|
||||
tags: undefined,
|
||||
onInlineAuthors: 'warn',
|
||||
};
|
||||
|
||||
const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||
|
@ -156,6 +157,9 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
|
|||
.disallow('')
|
||||
.allow(null, false)
|
||||
.default(() => DEFAULT_OPTIONS.tags),
|
||||
onInlineAuthors: Joi.string()
|
||||
.equal('ignore', 'log', 'warn', 'throw')
|
||||
.default(DEFAULT_OPTIONS.onInlineAuthors),
|
||||
}).default(DEFAULT_OPTIONS);
|
||||
|
||||
export function validateOptions({
|
||||
|
|
|
@ -44,6 +44,8 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
|||
};
|
||||
|
||||
export type Author = {
|
||||
key?: string; // TODO temporary, need refactor
|
||||
|
||||
/**
|
||||
* If `name` doesn't exist, an `imageURL` is expected.
|
||||
*/
|
||||
|
@ -442,6 +444,8 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
|
|||
* (filter, modify, delete, etc...).
|
||||
*/
|
||||
processBlogPosts: ProcessBlogPostsFn;
|
||||
/** The behavior of Docusaurus when it finds inline authors. */
|
||||
onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw';
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,7 +28,7 @@ export type TagsPluginOptions = {
|
|||
// TODO allow option tags later? | TagsFile;
|
||||
/** Path to the tags file. */
|
||||
tags: string | false | null | undefined;
|
||||
/** The behavior of Docusaurus when it found inline tags. */
|
||||
/** The behavior of Docusaurus when it finds inline tags. */
|
||||
onInlineTags: 'ignore' | 'log' | 'warn' | 'throw';
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
---
|
||||
tags:
|
||||
- b
|
||||
- label: d
|
||||
permalink: d-custom-permalink
|
||||
- d
|
||||
---
|
||||
|
||||
# Standalone doc
|
||||
|
|
|
@ -4,6 +4,7 @@ b:
|
|||
label: 'Label for tag b'
|
||||
c:
|
||||
permalink: '/permalink-for-tag-c'
|
||||
d:
|
||||
e:
|
||||
label: 'Label for tag e'
|
||||
description: 'Description for tag e'
|
||||
|
|
|
@ -93,6 +93,7 @@ export const dogfoodingPluginInstances: PluginConfig[] = [
|
|||
? undefined
|
||||
: defaultReadingTime({content, options: {wordsPerMinute: 5}}),
|
||||
onInlineTags: 'warn',
|
||||
onInlineAuthors: 'ignore',
|
||||
tags: 'tags.yml',
|
||||
} satisfies BlogOptions,
|
||||
],
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
---
|
||||
title: How I Converted Profilo to Docusaurus in Under 2 Hours
|
||||
author: Christine Abernathy
|
||||
authorURL: http://twitter.com/abernathyca
|
||||
authorImageURL: https://github.com/caabernathy.png
|
||||
authorTwitter: abernathyca
|
||||
authors: [abernathyca]
|
||||
tags: [profilo, adoption]
|
||||
---
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
---
|
||||
title: Towards Docusaurus 2
|
||||
author: Endilie Yacop Sucipto
|
||||
authorTitle: Maintainer of Docusaurus
|
||||
authorURL: https://github.com/endiliey
|
||||
authorImageURL: https://github.com/endiliey.png
|
||||
authorTwitter: endiliey
|
||||
authors: endiliey
|
||||
tags: [new, adoption]
|
||||
---
|
||||
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
---
|
||||
title: DocSearch migration
|
||||
authors:
|
||||
- name: Clément Vannicatte
|
||||
title: Software Engineer @ Algolia
|
||||
url: https://github.com/shortcuts
|
||||
image_url: https://github.com/shortcuts.png
|
||||
twitter: sh0rtcts
|
||||
- key: slorber
|
||||
|
||||
authors: [shortcuts, slorber]
|
||||
tags: [search]
|
||||
image: ./img/social-card.png
|
||||
---
|
||||
|
|
|
@ -43,3 +43,23 @@ Josh-Cena:
|
|||
url: https://joshcena.com/
|
||||
image_url: https://github.com/josh-cena.png
|
||||
email: sidachen2003@gmail.com
|
||||
|
||||
endiliey:
|
||||
name: Endilie Yacop Sucipto
|
||||
title: Maintainer of Docusaurus
|
||||
url: https://github.com/endiliey
|
||||
image_url: https://github.com/endiliey.png
|
||||
twitter: endiliey
|
||||
|
||||
abernathyca:
|
||||
name: Christine Abernathy
|
||||
url: http://twitter.com/abernathyca
|
||||
image_url: https://github.com/caabernathy.png
|
||||
twitter: abernathyca
|
||||
|
||||
shortcuts:
|
||||
name: Clément Vannicatte
|
||||
title: Software Engineer @ Algolia
|
||||
url: https://github.com/shortcuts
|
||||
image_url: https://github.com/shortcuts.png
|
||||
twitter: sh0rtcts
|
||||
|
|
|
@ -291,6 +291,7 @@ export default async function createConfigAsync() {
|
|||
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
|
||||
language: defaultLocale,
|
||||
},
|
||||
onInlineAuthors: 'warn',
|
||||
},
|
||||
],
|
||||
[
|
||||
|
|
Loading…
Add table
Reference in a new issue