From de59621fbbbee075f41629dbcc7e8c87de9d7baf Mon Sep 17 00:00:00 2001 From: ozaki <29860391+OzakIOne@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:40:18 +0200 Subject: [PATCH] feat(blog): warn duplicate and inline authors (#10224) Co-authored-by: OzakIOne Co-authored-by: sebastien --- .../src/__tests__/authors.test.ts | 2 +- .../src/__tests__/authorsProblems.test.ts | 178 ++++++++++++++++++ .../src/__tests__/feed.test.ts | 5 + .../src/authorsProblems.ts | 72 +++++++ .../src/blogUtils.ts | 7 + .../src/options.ts | 4 + .../src/plugin-content-blog.d.ts | 4 + packages/docusaurus-utils/src/tags.ts | 2 +- .../_dogfooding/_docs tests/standalone.mdx | 3 +- website/_dogfooding/_docs tests/tags.yml | 1 + website/_dogfooding/dogfooding.config.ts | 1 + ...-How-I-Converted-Profilo-To-Docusaurus.mdx | 5 +- .../blog/2018/09-11-Towards-Docusaurus-2.mdx | 6 +- .../index.mdx | 9 +- website/blog/authors.yml | 20 ++ website/docusaurus.config.ts | 1 + 16 files changed, 299 insertions(+), 21 deletions(-) create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/authorsProblems.test.ts create mode 100644 packages/docusaurus-plugin-content-blog/src/authorsProblems.ts diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts index 052437c3dec6..bb55d7b4c4ad 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/authors.test.ts @@ -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, diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/authorsProblems.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/authorsProblems.test.ts new file mode 100644 index 000000000000..84296df1c931 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/authorsProblems.test.ts @@ -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[0]['options']; + + function testReport({ + authors, + options = {}, + }: { + authors: Author[]; + options?: Partial; + }) { + 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 + ", + ] + `); + }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts index 8c79310890af..3fbd4dbf27fd 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -101,6 +101,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', + onInlineAuthors: 'ignore', } as PluginOptions, ); @@ -143,6 +144,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', + onInlineAuthors: 'ignore', } as PluginOptions, ); @@ -197,6 +199,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', + onInlineAuthors: 'ignore', } as PluginOptions, ); @@ -242,6 +245,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', + onInlineAuthors: 'ignore', } as PluginOptions, ); @@ -287,6 +291,7 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => { defaultReadingTime({content}), truncateMarker: //, onInlineTags: 'ignore', + onInlineAuthors: 'ignore', } as PluginOptions, ); diff --git a/packages/docusaurus-plugin-content-blog/src/authorsProblems.ts b/packages/docusaurus-plugin-content-blog/src/authorsProblems.ts new file mode 100644 index 000000000000..fecc7b5ce06c --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/authorsProblems.ts @@ -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; +}): void { + reportInlineAuthors(params); + reportDuplicateAuthors(params); +} + +export function reportInlineAuthors({ + authors, + blogSourceRelative, + options: {onInlineAuthors, authorsMapPath}, +}: { + authors: Author[]; + blogSourceRelative: string; + options: Pick; +}): 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); +} diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index d7a777ff77b6..68f429cc8c0c 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -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, diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index ade2e0e07e7b..68660ffa2b6f 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -58,6 +58,7 @@ export const DEFAULT_OPTIONS: PluginOptions = { processBlogPosts: async () => undefined, onInlineTags: 'warn', tags: undefined, + onInlineAuthors: 'warn', }; const PluginOptionSchema = Joi.object({ @@ -156,6 +157,9 @@ const PluginOptionSchema = Joi.object({ .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({ diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index f4d4f135c061..d3dbb699ec48 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -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'; }; /** diff --git a/packages/docusaurus-utils/src/tags.ts b/packages/docusaurus-utils/src/tags.ts index 1da758f9cd5d..fd06bf2fea25 100644 --- a/packages/docusaurus-utils/src/tags.ts +++ b/packages/docusaurus-utils/src/tags.ts @@ -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'; }; diff --git a/website/_dogfooding/_docs tests/standalone.mdx b/website/_dogfooding/_docs tests/standalone.mdx index c81811feae53..01edc102880f 100644 --- a/website/_dogfooding/_docs tests/standalone.mdx +++ b/website/_dogfooding/_docs tests/standalone.mdx @@ -1,8 +1,7 @@ --- tags: - b - - label: d - permalink: d-custom-permalink + - d --- # Standalone doc diff --git a/website/_dogfooding/_docs tests/tags.yml b/website/_dogfooding/_docs tests/tags.yml index 548ba5e7d862..9c4b9c73c18f 100644 --- a/website/_dogfooding/_docs tests/tags.yml +++ b/website/_dogfooding/_docs tests/tags.yml @@ -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' diff --git a/website/_dogfooding/dogfooding.config.ts b/website/_dogfooding/dogfooding.config.ts index 8ba4f58b71c0..e05203a191eb 100644 --- a/website/_dogfooding/dogfooding.config.ts +++ b/website/_dogfooding/dogfooding.config.ts @@ -93,6 +93,7 @@ export const dogfoodingPluginInstances: PluginConfig[] = [ ? undefined : defaultReadingTime({content, options: {wordsPerMinute: 5}}), onInlineTags: 'warn', + onInlineAuthors: 'ignore', tags: 'tags.yml', } satisfies BlogOptions, ], diff --git a/website/blog/2018/04-30-How-I-Converted-Profilo-To-Docusaurus.mdx b/website/blog/2018/04-30-How-I-Converted-Profilo-To-Docusaurus.mdx index bd2fa2e4044d..2c2eb734dc89 100644 --- a/website/blog/2018/04-30-How-I-Converted-Profilo-To-Docusaurus.mdx +++ b/website/blog/2018/04-30-How-I-Converted-Profilo-To-Docusaurus.mdx @@ -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] --- diff --git a/website/blog/2018/09-11-Towards-Docusaurus-2.mdx b/website/blog/2018/09-11-Towards-Docusaurus-2.mdx index 5496e46ae93a..584d969f0f5b 100644 --- a/website/blog/2018/09-11-Towards-Docusaurus-2.mdx +++ b/website/blog/2018/09-11-Towards-Docusaurus-2.mdx @@ -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] --- diff --git a/website/blog/2021/11-21-algolia-docsearch-migration/index.mdx b/website/blog/2021/11-21-algolia-docsearch-migration/index.mdx index 823f10a1659d..0c169bd357c4 100644 --- a/website/blog/2021/11-21-algolia-docsearch-migration/index.mdx +++ b/website/blog/2021/11-21-algolia-docsearch-migration/index.mdx @@ -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 --- diff --git a/website/blog/authors.yml b/website/blog/authors.yml index 5b84346cda7c..7fb45d8003ec 100644 --- a/website/blog/authors.yml +++ b/website/blog/authors.yml @@ -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 diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 093f70ec5a73..70d7fd33b78f 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -291,6 +291,7 @@ export default async function createConfigAsync() { copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, language: defaultLocale, }, + onInlineAuthors: 'warn', }, ], [