From 93593bc11ec6b62b447a7a972143d45edeef9f75 Mon Sep 17 00:00:00 2001 From: i511895 Date: Fri, 19 Nov 2021 18:28:45 +0100 Subject: [PATCH] Discussion on tag format --- .../tagsListFiles/tagsObjectSimple.json | 19 + .../tagsListFiles/tagsObjectSimple.yml | 6 + .../tagsListFiles/tagsObjects.json | 22 + .../tagsListFiles/tagsObjects.yml | 9 + .../tagsListFiles/tagsSimple.json | 19 + .../__fixtures__/tagsListFiles/tagsSimple.yml | 6 + .../src/__tests__/tags.test.ts | 573 ++++++++++++++++++ .../src/tags.ts | 49 ++ ...021-11-19-limit-tags-to-predefined-list.md | 7 + website/blog/authors.yml | 6 + 10 files changed, 716 insertions(+) create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.json create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.yml create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.json create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.yml create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.json create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.yml create mode 100644 packages/docusaurus-plugin-content-blog/src/__tests__/tags.test.ts create mode 100644 packages/docusaurus-plugin-content-blog/src/tags.ts create mode 100644 website/blog/2021-11-19-limit-tags-to-predefined-list.md diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.json b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.json new file mode 100644 index 000000000000..b1ea2f7ab0b8 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.json @@ -0,0 +1,19 @@ +{ + "tags": [ + { + "label": "i18n" + }, + { + "label": "release" + }, + { + "label": "recap" + }, + { + "label": "adoption" + }, + { + "label": "new" + } + ] +} \ No newline at end of file diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.yml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.yml new file mode 100644 index 000000000000..115028bf0a80 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjectSimple.yml @@ -0,0 +1,6 @@ +tags: + - label: i18n + - label: release + - label: recap + - label: adoption + - label: new \ No newline at end of file diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.json b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.json new file mode 100644 index 000000000000..4867978eb267 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.json @@ -0,0 +1,22 @@ +{ + "tags": [ + { + "label": "i18n", + "permalink": "/blog/i18n" + }, + { + "label": "release", + "permalink": "/blog/release" + }, + { + "label": "recap", + "permalink": "/blog/recap" + }, + { + "label": "adoption" + }, + { + "label": "new" + } + ] +} \ No newline at end of file diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.yml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.yml new file mode 100644 index 000000000000..00d813db7349 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsObjects.yml @@ -0,0 +1,9 @@ +tags: + - label: i18n + permalink: /blog/i18n + - label: release + permalink: /blog/release + - label: recap + permalink: /blog/release + - label: adoption + - label: new diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.json b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.json new file mode 100644 index 000000000000..b1ea2f7ab0b8 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.json @@ -0,0 +1,19 @@ +{ + "tags": [ + { + "label": "i18n" + }, + { + "label": "release" + }, + { + "label": "recap" + }, + { + "label": "adoption" + }, + { + "label": "new" + } + ] +} \ No newline at end of file diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.yml b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.yml new file mode 100644 index 000000000000..115028bf0a80 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/tagsListFiles/tagsSimple.yml @@ -0,0 +1,6 @@ +tags: + - label: i18n + - label: release + - label: recap + - label: adoption + - label: new \ No newline at end of file diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/tags.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/tags.test.ts new file mode 100644 index 000000000000..3695c96a23d2 --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/tags.test.ts @@ -0,0 +1,573 @@ +/** + * 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. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import {Tag} from '@docusaurus/utils'; +import { + getBlogPostTags, + getTagsList, + getTagsListFilePath, + readTagsListFile, + validateTagsListFile, +} from '../tags'; +import path from 'path'; + +describe('getBlogPostTags', () => { + test('can read no tags', () => { + expect( + getBlogPostTags({ + frontMatter: {}, + tagList: undefined, + }), + ).toEqual([]); + expect( + getBlogPostTags({ + frontMatter: { + tags: [], + }, + tagList: undefined, + }), + ).toEqual([]); + }); + + test('can read tags without tagList', () => { + expect( + getBlogPostTags({ + frontMatter: { + tags: ['tag-1', 'tag-2', 'tag-N'], + }, + tagList: undefined, + }), + ).toEqual(['tag-1', 'tag-2', 'tag-N']); + }); + + test('can read authors string', () => { + expect( + getBlogPostAuthors({ + frontMatter: { + authors: 'slorber', + }, + authorsMap: {slorber: {name: 'Sébastien Lorber'}}, + }), + ).toEqual([{key: 'slorber', name: 'Sébastien Lorber'}]); + }); + + test('can read authors string[]', () => { + expect( + getBlogPostAuthors({ + frontMatter: { + authors: ['slorber', 'yangshun'], + }, + authorsMap: { + slorber: {name: 'Sébastien Lorber', title: 'maintainer'}, + yangshun: {name: 'Yangshun Tay'}, + }, + }), + ).toEqual([ + {key: 'slorber', name: 'Sébastien Lorber', title: 'maintainer'}, + {key: 'yangshun', name: 'Yangshun Tay'}, + ]); + }); + + test('can read authors Author', () => { + expect( + getBlogPostAuthors({ + frontMatter: { + authors: {name: 'Sébastien Lorber', title: 'maintainer'}, + }, + authorsMap: undefined, + }), + ).toEqual([{name: 'Sébastien Lorber', title: 'maintainer'}]); + }); + + test('can read authors Author[]', () => { + expect( + getBlogPostAuthors({ + frontMatter: { + authors: [ + {name: 'Sébastien Lorber', title: 'maintainer'}, + {name: 'Yangshun Tay'}, + ], + }, + authorsMap: undefined, + }), + ).toEqual([ + {name: 'Sébastien Lorber', title: 'maintainer'}, + {name: 'Yangshun Tay'}, + ]); + }); + + test('can read authors complex (string | Author)[] setup with keys and local overrides', () => { + expect( + getBlogPostAuthors({ + frontMatter: { + authors: [ + 'slorber', + { + key: 'yangshun', + title: 'Yangshun title local override', + extra: 42, + }, + {name: 'Alexey'}, + ], + }, + authorsMap: { + slorber: {name: 'Sébastien Lorber', title: 'maintainer'}, + yangshun: {name: 'Yangshun Tay', title: 'Yangshun title original'}, + }, + }), + ).toEqual([ + {key: 'slorber', name: 'Sébastien Lorber', title: 'maintainer'}, + { + key: 'yangshun', + name: 'Yangshun Tay', + title: 'Yangshun title local override', + extra: 42, + }, + {name: 'Alexey'}, + ]); + }); + + test('throw when using author key with no authorsMap', () => { + expect(() => + getBlogPostAuthors({ + frontMatter: { + authors: 'slorber', + }, + authorsMap: undefined, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Can't reference blog post authors by a key (such as 'slorber') because no authors map file could be loaded. + Please double-check your blog plugin config (in particular 'authorsMapPath'), ensure the file exists at the configured path, is not empty, and is valid!" + `); + }); + + test('throw when using author key with empty authorsMap', () => { + expect(() => + getBlogPostAuthors({ + frontMatter: { + authors: 'slorber', + }, + authorsMap: {}, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Can't reference blog post authors by a key (such as 'slorber') because no authors map file could be loaded. + Please double-check your blog plugin config (in particular 'authorsMapPath'), ensure the file exists at the configured path, is not empty, and is valid!" + `); + }); + + test('throw when using bad author key in string', () => { + expect(() => + getBlogPostAuthors({ + frontMatter: { + authors: 'slorber', + }, + authorsMap: { + yangshun: {name: 'Yangshun Tay'}, + jmarcey: {name: 'Joel Marcey'}, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Blog author with key \\"slorber\\" not found in the authors map file. + Valid author keys are: + - yangshun + - jmarcey" + `); + }); + + test('throw when using bad author key in string[]', () => { + expect(() => + getBlogPostAuthors({ + frontMatter: { + authors: ['yangshun', 'jmarcey', 'slorber'], + }, + authorsMap: { + yangshun: {name: 'Yangshun Tay'}, + jmarcey: {name: 'Joel Marcey'}, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Blog author with key \\"slorber\\" not found in the authors map file. + Valid author keys are: + - yangshun + - jmarcey" + `); + }); + + test('throw when using bad author key in Author[].key', () => { + expect(() => + getBlogPostAuthors({ + frontMatter: { + authors: [{key: 'yangshun'}, {key: 'jmarcey'}, {key: 'slorber'}], + }, + authorsMap: { + yangshun: {name: 'Yangshun Tay'}, + jmarcey: {name: 'Joel Marcey'}, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Blog author with key \\"slorber\\" not found in the authors map file. + Valid author keys are: + - yangshun + - jmarcey" + `); + }); + + test('throw when mixing legacy/new authors frontmatter', () => { + expect(() => + getBlogPostAuthors({ + frontMatter: { + authors: [{name: 'Sébastien Lorber'}], + author: 'Yangshun Tay', + }, + authorsMap: undefined, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "To declare blog post authors, use the 'authors' FrontMatter in priority. + Don't mix 'authors' with other existing 'author_*' FrontMatter. Choose one or the other, not both at the same time." + `); + + expect(() => + getBlogPostAuthors({ + frontMatter: { + authors: [{key: 'slorber'}], + author_title: 'legacy title', + }, + authorsMap: {slorber: {name: 'Sébastien Lorber'}}, + }), + ).toThrowErrorMatchingInlineSnapshot(` + "To declare blog post authors, use the 'authors' FrontMatter in priority. + Don't mix 'authors' with other existing 'author_*' FrontMatter. Choose one or the other, not both at the same time." + `); + }); +}); + +describe('readAuthorsMapFile', () => { + const fixturesDir = path.join(__dirname, '__fixtures__/authorsMapFiles'); + + test('read valid yml author file', async () => { + const filePath = path.join(fixturesDir, 'authors.yml'); + expect(await readAuthorsMapFile(filePath)).toBeDefined(); + }); + + test('read valid json author file', async () => { + const filePath = path.join(fixturesDir, 'authors.json'); + expect(await readAuthorsMapFile(filePath)).toBeDefined(); + }); + + test('read yml and json should lead to the same result', async () => { + const content1 = await readAuthorsMapFile( + path.join(fixturesDir, 'authors.yml'), + ); + const content2 = await readAuthorsMapFile( + path.join(fixturesDir, 'authors.json'), + ); + expect(content1).toEqual(content2); + }); + + test('fail to read invalid yml 1', async () => { + const filePath = path.join(fixturesDir, 'authorsBad1.yml'); + await expect( + readAuthorsMapFile(filePath), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"\\"slorber.name\\" is required"`, + ); + }); + test('fail to read invalid json 1', async () => { + const filePath = path.join(fixturesDir, 'authorsBad1.json'); + await expect( + readAuthorsMapFile(filePath), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"\\"slorber.name\\" is required"`, + ); + }); + + test('fail to read invalid yml 2', async () => { + const filePath = path.join(fixturesDir, 'authorsBad2.yml'); + await expect( + readAuthorsMapFile(filePath), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"\\"name\\" must be of type object"`, + ); + }); + test('fail to read invalid json 2', async () => { + const filePath = path.join(fixturesDir, 'authorsBad2.json'); + await expect( + readAuthorsMapFile(filePath), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"\\"name\\" must be of type object"`, + ); + }); + + test('fail to read invalid yml 3', async () => { + const filePath = path.join(fixturesDir, 'authorsBad3.yml'); + await expect( + readAuthorsMapFile(filePath), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"\\"value\\" must be of type object"`, + ); + }); + test('fail to read invalid json 3', async () => { + const filePath = path.join(fixturesDir, 'authorsBad3.json'); + await expect( + readAuthorsMapFile(filePath), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"\\"value\\" must be of type object"`, + ); + }); +}); +describe('getAuthorsMap', () => { + const fixturesDir = path.join(__dirname, '__fixtures__/authorsMapFiles'); + const contentPaths = { + contentPathLocalized: fixturesDir, + contentPath: fixturesDir, + }; + + test('getAuthorsMap can read yml file', async () => { + expect( + await getAuthorsMap({ + contentPaths, + authorsMapPath: 'authors.yml', + }), + ).toBeDefined(); + }); + + test('getAuthorsMap can read json file', async () => { + expect( + await getAuthorsMap({ + contentPaths, + authorsMapPath: 'authors.json', + }), + ).toBeDefined(); + }); + + test('getAuthorsMap can return undefined if yaml file not found', async () => { + expect( + await getAuthorsMap({ + contentPaths, + authorsMapPath: 'authors_does_not_exist.yml', + }), + ).toBeUndefined(); + }); +}); + +describe('validateAuthorsMapFile', () => { + test('accept valid authors map', () => { + const authorsMap: AuthorsMap = { + slorber: { + name: 'Sébastien Lorber', + title: 'maintainer', + url: 'https://sebastienlorber.com', + imageURL: 'https://github.com/slorber.png', + }, + yangshun: { + name: 'Yangshun Tay', + imageURL: 'https://github.com/yangshun.png', + randomField: 42, + }, + jmarcey: { + name: 'Joel', + title: 'creator of Docusaurus', + hello: new Date(), + }, + }; + expect(validateAuthorsMapFile(authorsMap)).toEqual(authorsMap); + }); + + test('rename snake case image_url to camelCase imageURL', () => { + const authorsMap: AuthorsMap = { + slorber: { + name: 'Sébastien Lorber', + image_url: 'https://github.com/slorber.png', + }, + }; + expect(validateAuthorsMapFile(authorsMap)).toEqual({ + slorber: { + name: 'Sébastien Lorber', + imageURL: 'https://github.com/slorber.png', + }, + }); + }); + + test('reject author without name', () => { + const authorsMap: AuthorsMap = { + slorber: { + image_url: 'https://github.com/slorber.png', + }, + }; + expect(() => + validateAuthorsMapFile(authorsMap), + ).toThrowErrorMatchingInlineSnapshot(`"\\"slorber.name\\" is required"`); + }); + + test('reject undefined author', () => { + expect(() => + validateAuthorsMapFile({ + slorber: undefined, + }), + ).toThrowErrorMatchingInlineSnapshot(`"\\"slorber\\" is required"`); + }); + + test('reject null author', () => { + expect(() => + validateAuthorsMapFile({ + slorber: null, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"\\"slorber\\" must be of type object"`, + ); + }); + + test('reject array author', () => { + expect(() => + validateAuthorsMapFile({slorber: []}), + ).toThrowErrorMatchingInlineSnapshot( + `"\\"slorber\\" must be of type object"`, + ); + }); + + test('reject array content', () => { + expect(() => validateAuthorsMapFile([])).toThrowErrorMatchingInlineSnapshot( + // TODO improve this error message + `"\\"value\\" must be of type object"`, + ); + }); + + test('reject flat author', () => { + expect(() => + validateAuthorsMapFile({name: 'Sébastien'}), + ).toThrowErrorMatchingInlineSnapshot( + // TODO improve this error message + `"\\"name\\" must be of type object"`, + ); + }); + + test('reject non-map author', () => { + const authorsMap: AuthorsMap = { + // @ts-expect-error: for tests + slorber: [], + }; + expect(() => + validateAuthorsMapFile(authorsMap), + ).toThrowErrorMatchingInlineSnapshot( + `"\\"slorber\\" must be of type object"`, + ); + }); +}); + +describe('getAuthorsMapFilePath', () => { + const fixturesDir = path.join( + __dirname, + '__fixtures__/getAuthorsMapFilePath', + ); + const contentPathYml1 = path.join(fixturesDir, 'contentPathYml1'); + const contentPathYml2 = path.join(fixturesDir, 'contentPathYml2'); + const contentPathJson1 = path.join(fixturesDir, 'contentPathJson1'); + const contentPathJson2 = path.join(fixturesDir, 'contentPathJson2'); + const contentPathEmpty = path.join(fixturesDir, 'contentPathEmpty'); + const contentPathNestedYml = path.join(fixturesDir, 'contentPathNestedYml'); + + test('getAuthorsMapFilePath returns localized Yml path in priority', async () => { + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.yml', + contentPaths: { + contentPathLocalized: contentPathYml1, + contentPath: contentPathYml2, + }, + }), + ).toEqual(path.join(contentPathYml1, 'authors.yml')); + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.yml', + contentPaths: { + contentPathLocalized: contentPathYml2, + contentPath: contentPathYml1, + }, + }), + ).toEqual(path.join(contentPathYml2, 'authors.yml')); + }); + + test('getAuthorsMapFilePath returns localized Json path in priority', async () => { + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.json', + contentPaths: { + contentPathLocalized: contentPathJson1, + contentPath: contentPathJson2, + }, + }), + ).toEqual(path.join(contentPathJson1, 'authors.json')); + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.json', + contentPaths: { + contentPathLocalized: contentPathJson2, + contentPath: contentPathJson1, + }, + }), + ).toEqual(path.join(contentPathJson2, 'authors.json')); + }); + + test('getAuthorsMapFilePath returns unlocalized Yml path as fallback', async () => { + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.yml', + contentPaths: { + contentPathLocalized: contentPathEmpty, + contentPath: contentPathYml2, + }, + }), + ).toEqual(path.join(contentPathYml2, 'authors.yml')); + }); + + test('getAuthorsMapFilePath returns unlocalized Json path as fallback', async () => { + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.json', + contentPaths: { + contentPathLocalized: contentPathEmpty, + contentPath: contentPathJson1, + }, + }), + ).toEqual(path.join(contentPathJson1, 'authors.json')); + }); + + test('getAuthorsMapFilePath can return undefined (file not found)', async () => { + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.json', + contentPaths: { + contentPathLocalized: contentPathEmpty, + contentPath: contentPathYml1, + }, + }), + ).toBeUndefined(); + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'authors.yml', + contentPaths: { + contentPathLocalized: contentPathEmpty, + contentPath: contentPathJson1, + }, + }), + ).toBeUndefined(); + }); + + test('getAuthorsMapFilePath can return nested path', async () => { + expect( + await getAuthorsMapFilePath({ + authorsMapPath: 'sub/folder/authors.yml', + contentPaths: { + contentPathLocalized: contentPathEmpty, + contentPath: contentPathNestedYml, + }, + }), + ).toEqual(path.join(contentPathNestedYml, 'sub/folder/authors.yml')); + }); +}); diff --git a/packages/docusaurus-plugin-content-blog/src/tags.ts b/packages/docusaurus-plugin-content-blog/src/tags.ts new file mode 100644 index 000000000000..bbc458552f1b --- /dev/null +++ b/packages/docusaurus-plugin-content-blog/src/tags.ts @@ -0,0 +1,49 @@ +/** + * 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. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import {Tag} from '@docusaurus/utils'; +import {BlogPostFrontMatter} from './blogFrontMatter'; +import {BlogContentPaths} from './types'; + +type TagsParam = { + frontMatter: BlogPostFrontMatter; + tagList: Tag[] | undefined; +}; + +type TagsFileParams = { + tagsFilePath: string; + contentPaths: BlogContentPaths; +}; + +export function getBlogPostTags(params: TagsParam): Tag[] { + throw new Error('Not yet implemented'); +} + +export async function getTagsList( + params: TagsFileParams, +): Promise { + throw new Error('Not yet implemented'); +} + +export async function getTagsListFilePath({ + tagsFilePath, + contentPaths, +}: TagsFileParams): Promise { + throw new Error('Not yet implemented'); +} + +export function validateTagsListFile(content: unknown): Tag[] { + throw new Error('Not yet implemented'); +} + +export async function readTagsListFile( + filePath: string, +): Promise { + throw new Error('Not yet implemented'); +} diff --git a/website/blog/2021-11-19-limit-tags-to-predefined-list.md b/website/blog/2021-11-19-limit-tags-to-predefined-list.md new file mode 100644 index 000000000000..deaf05f9cfc8 --- /dev/null +++ b/website/blog/2021-11-19-limit-tags-to-predefined-list.md @@ -0,0 +1,7 @@ +--- +title: Releasing Docusaurus i18n +authors: [mambax] +tags: [tags] +--- + +Tags test diff --git a/website/blog/authors.yml b/website/blog/authors.yml index 63030834278a..49662db858ad 100644 --- a/website/blog/authors.yml +++ b/website/blog/authors.yml @@ -24,3 +24,9 @@ lex111: title: Open-source enthusiast url: https://github.com/lex111 image_url: https://github.com/lex111.png + +mambax: + name: Dominik Meyer + title: Engineering Principal at SAP + url: https://github.com/mambax + image_url: https://github.com/mambax.png