mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
feat(plugin-blog): multi-authors support + authors.yml global configuration (#5396)
* Complete function Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * A lot of blank lines Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * More lenient validation Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Remove or Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Simpler logic Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Expand docs Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Better docs Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Dogfood Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * More writeup Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Polish Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Polish Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Move mergeAuthorMap to authors.ts Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Unbreak relative assets * Update docs * Clarify in docs * simplify feed authors * rename authorMap -> authorsMap * mergeAuthorsMap -> getBlogPostAuthors * website => 5 blog posts per page * improve authors map file * Extract new theme authors components + display in row * add comment for meta array syntaxes * blog => getPathsToWatch should watch authorsMap file * remove useless v1 blog FBID frontmatter * keep older frontmatter syntax for now * revert blog frontmatter * Better console message * better blog authors frontmatter impl * add multi authors to beta blog post + fix some authors margins * fix React key * Refactor: mdx loader should support a more flexible assets system (poc, not documented yet) * better display of blog post authors: adapt layout to authors count + add line clamp * smaller local image * fix blog feed tests * fix blog frontmatter tests + improve validation schema * add more frontmatter tests * add tests for getAuthorsMapFilePath * tests for validateAuthorsMapFile * add tests for readAuthorsMapFile * test getAuthorsMap * exhaustive tests for getBlogPostAuthors * fix remaining tests * missing blog plugin author tests * fix windows tests * improve blog multi-author's doc * Use new format in init template Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * Improve error message Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * update feed snapshot * blog authors: limit to 2 cols + fix margins for no authors * minor doc improvements * better init template blog posts, demonstrating Blog features * replace the legacy blog author frontmatter in remaining places * Prefer using clsx Signed-off-by: Josh-Cena <sidachen2003@gmail.com> * cleanup getColClassName * remove blog author name/title line-clamping Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
8779c8ff4a
commit
493225a3c6
79 changed files with 1871 additions and 285 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -17,6 +17,7 @@ coverage
|
|||
.cache-loader
|
||||
types
|
||||
test-website
|
||||
test-website-in-workspace
|
||||
|
||||
packages/docusaurus/lib/
|
||||
packages/docusaurus-*/lib/*
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"workspaces": [
|
||||
"packages/*",
|
||||
"website",
|
||||
"test-website-in-workspace",
|
||||
"packages/docusaurus-init/templates/*",
|
||||
"admin/new.docusaurus.io"
|
||||
],
|
||||
|
|
|
@ -5,3 +5,27 @@ Create Docusaurus apps easily.
|
|||
## Usage
|
||||
|
||||
Please see the [installation documentation](https://docusaurus.io/docs/installation).
|
||||
|
||||
## For maintainers
|
||||
|
||||
For Docusaurus maintainers, templates can be tested with:
|
||||
|
||||
```bash
|
||||
cd `git rev-parse --show-toplevel` # Back to repo root
|
||||
rm -rf test-website
|
||||
yarn docusaurus-init init test-website classic
|
||||
cd test-website
|
||||
yarn start
|
||||
```
|
||||
|
||||
Note: `test-website` is not part of the workspace and use packages from npm.
|
||||
|
||||
Use the following to test the templates against local packages:
|
||||
|
||||
```bash
|
||||
cd `git rev-parse --show-toplevel` # Back to repo root
|
||||
rm -rf test-website-in-workspace
|
||||
yarn docusaurus-init init test-website-in-workspace classic
|
||||
cd test-website-in-workspace
|
||||
yarn start
|
||||
```
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
---
|
||||
slug: hola
|
||||
title: Hola
|
||||
author: Gao Wei
|
||||
author_title: Docusaurus Core Team
|
||||
author_url: https://github.com/wgao19
|
||||
author_image_url: https://avatars1.githubusercontent.com/u/2055384?v=4
|
||||
slug: first-blog-post
|
||||
title: First Blog Post
|
||||
authors:
|
||||
name: Gao Wei
|
||||
title: Docusaurus Core Team
|
||||
url: https://github.com/wgao19
|
||||
image_url: https://github.com/wgao19.png
|
||||
tags: [hola, docusaurus]
|
||||
---
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
slug: hello-world
|
||||
title: Hello
|
||||
author: Endilie Yacop Sucipto
|
||||
author_title: Maintainer of Docusaurus
|
||||
author_url: https://github.com/endiliey
|
||||
author_image_url: https://avatars1.githubusercontent.com/u/17883920?s=460&v=4
|
||||
tags: [hello, docusaurus]
|
||||
---
|
||||
|
||||
Welcome to this blog. This blog is created with [**Docusaurus 2**](https://docusaurus.io/).
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
This is a test post.
|
||||
|
||||
A whole bunch of other information.
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
slug: long-blog-post
|
||||
title: Long Blog Post
|
||||
authors: endi
|
||||
tags: [hello, docusaurus]
|
||||
---
|
||||
|
||||
This is the summary of a very long blog post,
|
||||
|
||||
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
slug: welcome
|
||||
title: Welcome
|
||||
author: Yangshun Tay
|
||||
author_title: Front End Engineer @ Facebook
|
||||
author_url: https://github.com/yangshun
|
||||
author_image_url: https://avatars0.githubusercontent.com/u/1315101?s=400&v=4
|
||||
tags: [facebook, hello, docusaurus]
|
||||
---
|
||||
|
||||
Blog features are powered by the blog plugin. Simply add files to the `blog` directory. It supports tags as well!
|
||||
|
||||
Delete the whole directory if you don't want the blog features. As simple as that!
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
slug: mdx-blog-post
|
||||
title: MDX Blog Post
|
||||
authors: [slorber]
|
||||
tags: [docusaurus]
|
||||
---
|
||||
|
||||
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
|
||||
|
||||
:::tip
|
||||
|
||||
Use the power of React to create interactive blog posts.
|
||||
|
||||
```js
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||
```
|
||||
|
||||
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||
|
||||
:::
|
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
slug: welcome
|
||||
title: Welcome
|
||||
authors: [slorber, yangshun]
|
||||
tags: [facebook, hello, docusaurus]
|
||||
---
|
||||
|
||||
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
|
||||
|
||||
Simply add Markdown files (or folders) to the `blog` directory.
|
||||
|
||||
Regular blog authors can be added to `authors.yml`.
|
||||
|
||||
The blog post date can be extracted from filenames, such as:
|
||||
|
||||
- `2019-05-30-welcome.md`
|
||||
- `2019-05-30-welcome/index.md`
|
||||
|
||||
A blog post folder can be convenient to co-locate blog post images:
|
||||
|
||||

|
||||
|
||||
The blog supports tags as well!
|
||||
|
||||
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
|
17
packages/docusaurus-init/templates/shared/blog/authors.yml
Normal file
17
packages/docusaurus-init/templates/shared/blog/authors.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
endi:
|
||||
name: Endilie Yacop Sucipto
|
||||
title: Maintainer of Docusaurus
|
||||
url: https://github.com/endiliey
|
||||
image_url: https://github.com/endiliey.png
|
||||
|
||||
yangshun:
|
||||
name: Yangshun Tay
|
||||
title: Front End Engineer @ Facebook
|
||||
url: https://github.com/yangshun
|
||||
image_url: https://github.com/yangshun.png
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
|
@ -14,10 +14,15 @@ Create a file at `blog/2021-02-28-greetings.md`:
|
|||
---
|
||||
slug: greetings
|
||||
title: Greetings!
|
||||
author: Steven Hansel
|
||||
author_title: Docusaurus Contributor
|
||||
author_url: https://github.com/ShinteiMai
|
||||
author_image_url: https://github.com/ShinteiMai.png
|
||||
authors:
|
||||
- name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
- name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
tags: [greetings]
|
||||
---
|
||||
|
||||
|
|
|
@ -53,43 +53,45 @@ async function readMetadataPath(metadataPath: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// For some specific FrontMatter fields, we want to allow referencing local relative assets so that they enter the Webpack asset pipeline
|
||||
// We don't do that for all frontMatters, only for the configured keys
|
||||
// Converts assets an object with Webpack require calls code
|
||||
// This is useful for mdx files to reference co-located assets using relative paths
|
||||
// Those assets should enter the Webpack assets pipeline and be hashed
|
||||
// For now, we only handle that for images and paths starting with ./
|
||||
// {image: "./myImage.png"} => {image: require("./myImage.png")}
|
||||
function createFrontMatterAssetsExportCode(
|
||||
frontMatter: Record<string, unknown>,
|
||||
frontMatterAssetKeys: string[] = [],
|
||||
) {
|
||||
if (frontMatterAssetKeys.length === 0) {
|
||||
function createAssetsExportCode(assets: Record<string, unknown>) {
|
||||
if (Object.keys(assets).length === 0) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
function createFrontMatterAssetRequireCode(value: unknown) {
|
||||
// TODO implementation can be completed/enhanced
|
||||
function createAssetValueCode(assetValue: unknown): string | undefined {
|
||||
if (Array.isArray(assetValue)) {
|
||||
const arrayItemCodes = assetValue.map(
|
||||
(item) => createAssetValueCode(item) ?? 'undefined',
|
||||
);
|
||||
return `[${arrayItemCodes.join(', ')}]`;
|
||||
}
|
||||
// Only process string values starting with ./
|
||||
// We could enhance this logic and check if file exists on disc?
|
||||
if (typeof value === 'string' && value.startsWith('./')) {
|
||||
if (typeof assetValue === 'string' && assetValue.startsWith('./')) {
|
||||
// TODO do we have other use-cases than image assets?
|
||||
// Probably not worth adding more support, as we want to move to Webpack 5 new asset system (https://github.com/facebook/docusaurus/pull/4708)
|
||||
const inlineLoader = inlineMarkdownImageFileLoader;
|
||||
return `require("${inlineLoader}${escapePath(value)}").default`;
|
||||
return `require("${inlineLoader}${escapePath(assetValue)}").default`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const frontMatterAssetEntries = Object.entries(frontMatter).filter(([key]) =>
|
||||
frontMatterAssetKeys.includes(key),
|
||||
);
|
||||
const assetEntries = Object.entries(assets);
|
||||
|
||||
const lines = frontMatterAssetEntries
|
||||
const codeLines = assetEntries
|
||||
.map(([key, value]) => {
|
||||
const assetRequireCode = createFrontMatterAssetRequireCode(value);
|
||||
const assetRequireCode = createAssetValueCode(value);
|
||||
return assetRequireCode ? `"${key}": ${assetRequireCode},` : undefined;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const exportValue = `{\n${lines.join('\n')}\n}`;
|
||||
|
||||
return exportValue;
|
||||
return `{\n${codeLines.join('\n')}\n}`;
|
||||
}
|
||||
|
||||
const docusaurusMdxLoader: Loader = async function (fileString) {
|
||||
|
@ -130,18 +132,9 @@ const docusaurusMdxLoader: Loader = async function (fileString) {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
let exportStr = `
|
||||
export const frontMatter = ${stringifyObject(frontMatter)};
|
||||
export const frontMatterAssets = ${createFrontMatterAssetsExportCode(
|
||||
frontMatter,
|
||||
reqOptions.frontMatterAssetKeys,
|
||||
)};
|
||||
export const contentTitle = ${stringifyObject(contentTitle)};`;
|
||||
|
||||
// MDX partials are MDX files starting with _ or in a folder starting with _
|
||||
// Partial are not expected to have an associated metadata file or frontmatter
|
||||
const isMDXPartial = options.isMDXPartial && options.isMDXPartial(filePath);
|
||||
|
||||
if (isMDXPartial && hasFrontMatter) {
|
||||
const errorMessage = `Docusaurus MDX partial files should not contain FrontMatter.
|
||||
Those partial files use the _ prefix as a convention by default, but this is configurable.
|
||||
|
@ -158,26 +151,46 @@ ${JSON.stringify(frontMatter, null, 2)}`;
|
|||
}
|
||||
}
|
||||
|
||||
if (!isMDXPartial) {
|
||||
// Read metadata for this MDX and export it.
|
||||
if (options.metadataPath && typeof options.metadataPath === 'function') {
|
||||
const metadataPath = options.metadataPath(filePath);
|
||||
|
||||
if (metadataPath) {
|
||||
const metadata = await readMetadataPath(metadataPath);
|
||||
exportStr += `\nexport const metadata = ${metadata};`;
|
||||
// Add as dependency of this loader result so that we can
|
||||
// recompile if metadata is changed.
|
||||
this.addDependency(metadataPath);
|
||||
function getMetadataPath(): string | undefined {
|
||||
if (!isMDXPartial) {
|
||||
// Read metadata for this MDX and export it.
|
||||
if (options.metadataPath && typeof options.metadataPath === 'function') {
|
||||
return options.metadataPath(filePath);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const metadataPath = getMetadataPath();
|
||||
if (metadataPath) {
|
||||
this.addDependency(metadataPath);
|
||||
}
|
||||
|
||||
const metadataJsonString = metadataPath
|
||||
? await readMetadataPath(metadataPath)
|
||||
: undefined;
|
||||
|
||||
const metadata = metadataJsonString
|
||||
? JSON.parse(metadataJsonString)
|
||||
: undefined;
|
||||
|
||||
const assets =
|
||||
reqOptions.createAssets && metadata
|
||||
? reqOptions.createAssets({frontMatter, metadata})
|
||||
: undefined;
|
||||
|
||||
const exportsCode = `
|
||||
export const frontMatter = ${stringifyObject(frontMatter)};
|
||||
export const contentTitle = ${stringifyObject(contentTitle)};
|
||||
${metadataJsonString ? `export const metadata = ${metadataJsonString};` : ''}
|
||||
${assets ? `export const assets = ${createAssetsExportCode(assets)};` : ''}
|
||||
`;
|
||||
|
||||
const code = `
|
||||
import React from 'react';
|
||||
import { mdx } from '@mdx-js/react';
|
||||
|
||||
${exportStr}
|
||||
${exportsCode}
|
||||
${result}
|
||||
`;
|
||||
|
||||
|
|
|
@ -21,33 +21,10 @@ declare module '@theme/BlogSidebar' {
|
|||
}
|
||||
|
||||
declare module '@theme/BlogPostPage' {
|
||||
import type {FrontMatterTag} from '@docusaurus/utils';
|
||||
import type {BlogSidebar} from '@theme/BlogSidebar';
|
||||
|
||||
export type FrontMatter = {
|
||||
/* eslint-disable camelcase */
|
||||
readonly title: string;
|
||||
readonly author?: string;
|
||||
readonly image?: string;
|
||||
readonly tags?: readonly FrontMatterTag[];
|
||||
readonly keywords?: readonly string[];
|
||||
readonly author_url?: string;
|
||||
readonly authorURL?: string;
|
||||
readonly author_title?: string;
|
||||
readonly authorTitle?: string;
|
||||
readonly author_image_url?: string;
|
||||
readonly authorImageURL?: string;
|
||||
readonly hide_table_of_contents?: boolean;
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
|
||||
export type FrontMatterAssets = {
|
||||
/* eslint-disable camelcase */
|
||||
readonly image?: string;
|
||||
readonly author_image_url?: string;
|
||||
readonly authorImageURL?: string;
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
export type FrontMatter = import('./src/blogFrontMatter').BlogPostFrontMatter;
|
||||
export type Assets = import('./src/types').Assets;
|
||||
|
||||
export type Metadata = {
|
||||
readonly title: string;
|
||||
|
@ -60,6 +37,7 @@ declare module '@theme/BlogPostPage' {
|
|||
readonly truncated?: string;
|
||||
readonly nextItem?: {readonly title: string; readonly permalink: string};
|
||||
readonly prevItem?: {readonly title: string; readonly permalink: string};
|
||||
readonly authors: import('./src/types').Author[];
|
||||
readonly tags: readonly {
|
||||
readonly label: string;
|
||||
readonly permalink: string;
|
||||
|
@ -68,7 +46,7 @@ declare module '@theme/BlogPostPage' {
|
|||
|
||||
export type Content = {
|
||||
readonly frontMatter: FrontMatter;
|
||||
readonly frontMatterAssets: FrontMatterAssets;
|
||||
readonly assets: Assets;
|
||||
readonly metadata: Metadata;
|
||||
readonly toc: readonly TOCItem[];
|
||||
(): JSX.Element;
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"feed": "^4.2.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"globby": "^11.0.2",
|
||||
"js-yaml": "^4.0.0",
|
||||
"loader-utils": "^2.0.0",
|
||||
"lodash": "^4.17.20",
|
||||
"reading-time": "^1.3.0",
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"JMarcey": {
|
||||
"name": "Joel Marcey",
|
||||
"title": "Technical Lead & Developer Advocate at Facebook",
|
||||
"url": "http://twitter.com/JoelMarcey",
|
||||
"image_url": "https://github.com/JoelMarcey.png",
|
||||
"twitter": "JoelMarcey"
|
||||
},
|
||||
"slorber": {
|
||||
"name": "Sébastien Lorber",
|
||||
"title": "Docusaurus maintainer",
|
||||
"url": "https://sebastienlorber.com",
|
||||
"image_url": "https://github.com/slorber.png",
|
||||
"twitter": "sebastienlorber"
|
||||
},
|
||||
"yangshun": {
|
||||
"name": "Yangshun Tay",
|
||||
"title": "Front End Engineer at Facebook",
|
||||
"url": "https://github.com/yangshun",
|
||||
"image_url": "https://github.com/yangshun.png",
|
||||
"twitter": "yangshunz"
|
||||
},
|
||||
"lex111": {
|
||||
"name": "Alexey Pyltsyn",
|
||||
"title": "Open-source enthusiast",
|
||||
"url": "https://github.com/lex111",
|
||||
"image_url": "https://github.com/lex111.png"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
JMarcey:
|
||||
name: Joel Marcey
|
||||
title: Technical Lead & Developer Advocate at Facebook
|
||||
url: http://twitter.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
twitter: JoelMarcey
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
twitter: sebastienlorber
|
||||
|
||||
yangshun:
|
||||
name: Yangshun Tay
|
||||
title: Front End Engineer at Facebook
|
||||
url: https://github.com/yangshun
|
||||
image_url: https://github.com/yangshun.png
|
||||
twitter: yangshunz
|
||||
|
||||
lex111:
|
||||
name: Alexey Pyltsyn
|
||||
title: Open-source enthusiast
|
||||
url: https://github.com/lex111
|
||||
image_url: https://github.com/lex111.png
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"slorber": {
|
||||
"title": "Docusaurus maintainer"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
slorber:
|
||||
title: Docusaurus maintainer
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"name": "Sébastien Lorber"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
name: Sébastien Lorber
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"name": "Sébastien Lorber"
|
||||
},
|
||||
{
|
||||
"name": "Joel Marcey"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
- name: Sébastien Lorber
|
||||
- name: Joel Marcey
|
|
@ -1,5 +1,8 @@
|
|||
---
|
||||
title: Happy 1st Birthday Slash!
|
||||
authors:
|
||||
- name: Yangshun Tay
|
||||
- slorber
|
||||
---
|
||||
|
||||
Happy birthday!
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
|
@ -2,6 +2,10 @@
|
|||
slug: /simple/slug
|
||||
title: Simple Slug
|
||||
date: 2020-08-15
|
||||
|
||||
author: Sébastien Lorber
|
||||
author_title: Docusaurus maintainer
|
||||
author_url: https://sebastienlorber.com
|
||||
---
|
||||
|
||||
simple url slug
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
---
|
||||
title: Happy 1st Birthday Slash! (translated)
|
||||
authors:
|
||||
- name: Yangshun Tay (translated)
|
||||
- slorber
|
||||
---
|
||||
|
||||
Happy birthday! (translated)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber (translated)
|
||||
title: Docusaurus maintainer (translated)
|
|
@ -26,6 +26,10 @@ exports[`blogFeed atom shows feed item for each post 1`] = `
|
|||
<link href=\\"https://docusaurus.io/myBaseUrl/blog/simple/slug\\"/>
|
||||
<updated>2020-08-15T00:00:00.000Z</updated>
|
||||
<summary type=\\"html\\"><![CDATA[simple url slug]]></summary>
|
||||
<author>
|
||||
<name>Sébastien Lorber</name>
|
||||
<uri>https://sebastienlorber.com</uri>
|
||||
</author>
|
||||
</entry>
|
||||
<entry>
|
||||
<title type=\\"html\\"><![CDATA[draft]]></title>
|
||||
|
@ -53,6 +57,12 @@ exports[`blogFeed atom shows feed item for each post 1`] = `
|
|||
<link href=\\"https://docusaurus.io/myBaseUrl/blog/2018/12/14/Happy-First-Birthday-Slash\\"/>
|
||||
<updated>2018-12-14T00:00:00.000Z</updated>
|
||||
<summary type=\\"html\\"><![CDATA[Happy birthday! (translated)]]></summary>
|
||||
<author>
|
||||
<name>Yangshun Tay (translated)</name>
|
||||
</author>
|
||||
<author>
|
||||
<name>Sébastien Lorber (translated)</name>
|
||||
</author>
|
||||
</entry>
|
||||
</feed>"
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,608 @@
|
|||
/**
|
||||
* 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 {
|
||||
AuthorsMap,
|
||||
getAuthorsMapFilePath,
|
||||
validateAuthorsMapFile,
|
||||
readAuthorsMapFile,
|
||||
getAuthorsMap,
|
||||
getBlogPostAuthors,
|
||||
} from '../authors';
|
||||
import path from 'path';
|
||||
|
||||
describe('getBlogPostAuthors', () => {
|
||||
test('can read no authors', () => {
|
||||
expect(
|
||||
getBlogPostAuthors({
|
||||
frontMatter: {},
|
||||
authorsMap: undefined,
|
||||
}),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
getBlogPostAuthors({
|
||||
frontMatter: {
|
||||
authors: [],
|
||||
},
|
||||
authorsMap: undefined,
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('can read author from legacy frontmatter', () => {
|
||||
expect(
|
||||
getBlogPostAuthors({
|
||||
frontMatter: {
|
||||
author: 'Sébastien Lorber',
|
||||
},
|
||||
authorsMap: undefined,
|
||||
}),
|
||||
).toEqual([{name: 'Sébastien Lorber'}]);
|
||||
expect(
|
||||
getBlogPostAuthors({
|
||||
frontMatter: {
|
||||
authorTitle: 'maintainer',
|
||||
},
|
||||
authorsMap: undefined,
|
||||
}),
|
||||
).toEqual([{title: 'maintainer'}]);
|
||||
expect(
|
||||
getBlogPostAuthors({
|
||||
frontMatter: {
|
||||
authorImageURL: 'https://github.com/slorber.png',
|
||||
},
|
||||
authorsMap: undefined,
|
||||
}),
|
||||
).toEqual([{imageURL: 'https://github.com/slorber.png'}]);
|
||||
expect(
|
||||
getBlogPostAuthors({
|
||||
frontMatter: {
|
||||
author: 'Sébastien Lorber',
|
||||
author_title: 'maintainer1',
|
||||
authorTitle: 'maintainer2',
|
||||
author_image_url: 'https://github.com/slorber1.png',
|
||||
authorImageURL: 'https://github.com/slorber2.png',
|
||||
author_url: 'https://github.com/slorber1',
|
||||
authorURL: 'https://github.com/slorber2',
|
||||
},
|
||||
authorsMap: undefined,
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
name: 'Sébastien Lorber',
|
||||
title: 'maintainer1',
|
||||
imageURL: 'https://github.com/slorber1.png',
|
||||
url: 'https://github.com/slorber1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
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'));
|
||||
});
|
||||
});
|
|
@ -11,6 +11,8 @@ import {
|
|||
} from '../blogFrontMatter';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
|
||||
// TODO this abstraction reduce verbosity but it makes it harder to debug
|
||||
// It would be preferable to just expose helper methods
|
||||
function testField(params: {
|
||||
fieldName: keyof BlogPostFrontMatter;
|
||||
validFrontMatters: BlogPostFrontMatter[];
|
||||
|
@ -99,7 +101,30 @@ describe('validateBlogPostFrontMatter id', () => {
|
|||
testField({
|
||||
fieldName: 'id',
|
||||
validFrontMatters: [{id: '123'}, {id: 'id'}],
|
||||
invalidFrontMatters: [[{id: ''}, 'is not allowed to be empty']],
|
||||
invalidFrontMatters: [[{id: ''}, 'not allowed to be empty']],
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateBlogPostFrontMatter handles legacy/new author frontmatter', () => {
|
||||
test('allow legacy author frontmatter', () => {
|
||||
const frontMatter: BlogPostFrontMatter = {
|
||||
author: 'Sebastien',
|
||||
author_url: 'https://sebastienlorber.com',
|
||||
author_title: 'maintainer',
|
||||
author_image_url: 'https://github.com/slorber.png',
|
||||
};
|
||||
expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
|
||||
});
|
||||
|
||||
test('allow new authors frontmatter', () => {
|
||||
const frontMatter: BlogPostFrontMatter = {
|
||||
authors: [
|
||||
'slorber',
|
||||
{name: 'Yangshun'},
|
||||
{key: 'JMarcey', title: 'creator', random: '42'},
|
||||
],
|
||||
};
|
||||
expect(validateBlogPostFrontMatter(frontMatter)).toEqual(frontMatter);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -107,21 +132,24 @@ describe('validateBlogPostFrontMatter author', () => {
|
|||
testField({
|
||||
fieldName: 'author',
|
||||
validFrontMatters: [{author: '123'}, {author: 'author'}],
|
||||
invalidFrontMatters: [[{author: ''}, 'is not allowed to be empty']],
|
||||
invalidFrontMatters: [[{author: ''}, 'not allowed to be empty']],
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateBlogPostFrontMatter author_title', () => {
|
||||
testField({
|
||||
fieldName: 'author_title',
|
||||
validFrontMatters: [{author_title: '123'}, {author_title: 'author_title'}],
|
||||
invalidFrontMatters: [[{author_title: ''}, 'is not allowed to be empty']],
|
||||
validFrontMatters: [
|
||||
{author: '123', author_title: '123'},
|
||||
{author: '123', author_title: 'author_title'},
|
||||
],
|
||||
invalidFrontMatters: [[{author_title: ''}, 'not allowed to be empty']],
|
||||
});
|
||||
|
||||
testField({
|
||||
fieldName: 'authorTitle',
|
||||
validFrontMatters: [{authorTitle: '123'}, {authorTitle: 'authorTitle'}],
|
||||
invalidFrontMatters: [[{authorTitle: ''}, 'is not allowed to be empty']],
|
||||
invalidFrontMatters: [[{authorTitle: ''}, 'not allowed to be empty']],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -136,7 +164,7 @@ describe('validateBlogPostFrontMatter author_url', () => {
|
|||
invalidFrontMatters: [
|
||||
[
|
||||
{author_url: ''},
|
||||
'"author_url" does not match any of the allowed types',
|
||||
'"author_url" does not look like a valid url (value=\'\')',
|
||||
],
|
||||
],
|
||||
});
|
||||
|
@ -150,7 +178,10 @@ describe('validateBlogPostFrontMatter author_url', () => {
|
|||
],
|
||||
|
||||
invalidFrontMatters: [
|
||||
[{authorURL: ''}, '"authorURL" does not match any of the allowed types'],
|
||||
[
|
||||
{authorURL: ''},
|
||||
'"authorURL" does not look like a valid url (value=\'\')',
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -166,7 +197,7 @@ describe('validateBlogPostFrontMatter author_image_url', () => {
|
|||
invalidFrontMatters: [
|
||||
[
|
||||
{author_image_url: ''},
|
||||
'"author_image_url" does not match any of the allowed types',
|
||||
'"author_image_url" does not look like a valid url (value=\'\')',
|
||||
],
|
||||
],
|
||||
});
|
||||
|
@ -181,7 +212,55 @@ describe('validateBlogPostFrontMatter author_image_url', () => {
|
|||
invalidFrontMatters: [
|
||||
[
|
||||
{authorImageURL: ''},
|
||||
'"authorImageURL" does not match any of the allowed types',
|
||||
'"authorImageURL" does not look like a valid url (value=\'\')',
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateBlogPostFrontMatter authors', () => {
|
||||
testField({
|
||||
fieldName: 'author',
|
||||
validFrontMatters: [
|
||||
{authors: []},
|
||||
{authors: 'authorKey'},
|
||||
{authors: ['authorKey1', 'authorKey2']},
|
||||
{
|
||||
authors: {
|
||||
name: 'Author Name',
|
||||
imageURL: '/absolute',
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: {
|
||||
key: 'authorKey',
|
||||
title: 'Author title',
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: [
|
||||
'authorKey1',
|
||||
{key: 'authorKey3'},
|
||||
'authorKey3',
|
||||
{name: 'Author Name 4'},
|
||||
{key: 'authorKey5'},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
invalidFrontMatters: [
|
||||
[{authors: ''}, '"authors" is not allowed to be empty'],
|
||||
[
|
||||
{authors: [undefined]},
|
||||
'"authors[0]" does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).',
|
||||
],
|
||||
[
|
||||
{authors: [null]},
|
||||
'"authors[0]" does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).',
|
||||
],
|
||||
[
|
||||
{authors: [{}]},
|
||||
'"authors[0]" does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).',
|
||||
],
|
||||
],
|
||||
});
|
||||
|
@ -200,7 +279,7 @@ describe('validateBlogPostFrontMatter slug', () => {
|
|||
{slug: '/api/plugins/@docusaurus/plugin-debug'},
|
||||
{slug: '@site/api/asset/image.png'},
|
||||
],
|
||||
invalidFrontMatters: [[{slug: ''}, 'is not allowed to be empty']],
|
||||
invalidFrontMatters: [[{slug: ''}, 'not allowed to be empty']],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -219,7 +298,7 @@ describe('validateBlogPostFrontMatter image', () => {
|
|||
{image: '@site/api/asset/image.png'},
|
||||
],
|
||||
invalidFrontMatters: [
|
||||
[{image: ''}, '"image" does not match any of the allowed types'],
|
||||
[{image: ''}, '"image" does not look like a valid url (value=\'\')'],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -236,7 +315,7 @@ describe('validateBlogPostFrontMatter tags', () => {
|
|||
],
|
||||
invalidFrontMatters: [
|
||||
[{tags: ''}, 'must be an array'],
|
||||
[{tags: ['']}, 'is not allowed to be empty'],
|
||||
[{tags: ['']}, 'not allowed to be empty'],
|
||||
],
|
||||
// See https://github.com/facebook/docusaurus/issues/4642
|
||||
convertibleFrontMatter: [
|
||||
|
@ -260,7 +339,7 @@ describe('validateBlogPostFrontMatter keywords', () => {
|
|||
],
|
||||
invalidFrontMatters: [
|
||||
[{keywords: ''}, 'must be an array'],
|
||||
[{keywords: ['']}, 'is not allowed to be empty'],
|
||||
[{keywords: ['']}, 'not allowed to be empty'],
|
||||
[{keywords: []}, 'does not contain 1 required value(s)'],
|
||||
],
|
||||
});
|
||||
|
@ -304,9 +383,7 @@ describe('validateBlogPostFrontMatter date', () => {
|
|||
fieldName: 'date',
|
||||
validFrontMatters: [
|
||||
{date: new Date('2020-01-01')},
|
||||
// @ts-expect-error: string for test
|
||||
{date: '2020-01-01'},
|
||||
// @ts-expect-error: string for test
|
||||
{date: '2020'},
|
||||
],
|
||||
invalidFrontMatters: [
|
||||
|
|
|
@ -52,6 +52,7 @@ describe('blogFeed', () => {
|
|||
{
|
||||
path: 'invalid-blog-path',
|
||||
routeBasePath: 'blog',
|
||||
authorsMapPath: 'authors.yml',
|
||||
include: ['*.md', '*.mdx'],
|
||||
feedOptions: {
|
||||
type: [feedType],
|
||||
|
@ -85,6 +86,7 @@ describe('blogFeed', () => {
|
|||
{
|
||||
path: 'blog',
|
||||
routeBasePath: 'blog',
|
||||
authorsMapPath: 'authors.yml',
|
||||
include: DEFAULT_OPTIONS.include,
|
||||
exclude: DEFAULT_OPTIONS.exclude,
|
||||
feedOptions: {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
|
|||
import {PluginOptionSchema} from '../pluginOptionSchema';
|
||||
import {PluginOptions, EditUrlFunction, BlogPost} from '../types';
|
||||
import {Joi} from '@docusaurus/utils-validation';
|
||||
import {posixPath} from '@docusaurus/utils';
|
||||
|
||||
function findByTitle(
|
||||
blogPosts: BlogPost[],
|
||||
|
@ -60,7 +61,7 @@ describe('loadBlog', () => {
|
|||
|
||||
const BaseEditUrl = 'https://baseEditUrl.com/edit';
|
||||
|
||||
const getBlogPosts = async (
|
||||
const getPlugin = async (
|
||||
siteDir: string,
|
||||
pluginOptions: Partial<PluginOptions> = {},
|
||||
i18n: I18n = DefaultI18N,
|
||||
|
@ -71,7 +72,7 @@ describe('loadBlog', () => {
|
|||
baseUrl: '/',
|
||||
url: 'https://docusaurus.io',
|
||||
} as DocusaurusConfig;
|
||||
const plugin = pluginContentBlog(
|
||||
return pluginContentBlog(
|
||||
{
|
||||
siteDir,
|
||||
siteConfig,
|
||||
|
@ -84,11 +85,32 @@ describe('loadBlog', () => {
|
|||
...pluginOptions,
|
||||
}),
|
||||
);
|
||||
const {blogPosts} = (await plugin.loadContent!())!;
|
||||
};
|
||||
|
||||
const getBlogPosts = async (
|
||||
siteDir: string,
|
||||
pluginOptions: Partial<PluginOptions> = {},
|
||||
i18n: I18n = DefaultI18N,
|
||||
) => {
|
||||
const plugin = await getPlugin(siteDir, pluginOptions, i18n);
|
||||
const {blogPosts} = (await plugin.loadContent!())!;
|
||||
return blogPosts;
|
||||
};
|
||||
|
||||
test('getPathsToWatch', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const plugin = await getPlugin(siteDir);
|
||||
const pathsToWatch = plugin.getPathsToWatch!();
|
||||
const relativePathsToWatch = pathsToWatch.map((p) =>
|
||||
posixPath(path.relative(siteDir, p)),
|
||||
);
|
||||
expect(relativePathsToWatch).toEqual([
|
||||
'blog/authors.yml',
|
||||
'i18n/en/docusaurus-plugin-content-blog/**/*.{md,mdx}',
|
||||
'blog/**/*.{md,mdx}',
|
||||
]);
|
||||
});
|
||||
|
||||
test('simple website', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const blogPosts = await getBlogPosts(siteDir);
|
||||
|
@ -103,6 +125,7 @@ describe('loadBlog', () => {
|
|||
source: path.posix.join('@site', PluginPath, 'date-matter.md'),
|
||||
title: 'date-matter',
|
||||
description: `date inside front matter`,
|
||||
authors: [],
|
||||
date: new Date('2019-01-01'),
|
||||
formattedDate: 'January 1, 2019',
|
||||
prevItem: undefined,
|
||||
|
@ -128,6 +151,16 @@ describe('loadBlog', () => {
|
|||
),
|
||||
title: 'Happy 1st Birthday Slash! (translated)',
|
||||
description: `Happy birthday! (translated)`,
|
||||
authors: [
|
||||
{
|
||||
name: 'Yangshun Tay (translated)',
|
||||
},
|
||||
{
|
||||
key: 'slorber',
|
||||
name: 'Sébastien Lorber (translated)',
|
||||
title: 'Docusaurus maintainer (translated)',
|
||||
},
|
||||
],
|
||||
date: new Date('2018-12-14'),
|
||||
formattedDate: 'December 14, 2018',
|
||||
tags: [],
|
||||
|
@ -148,6 +181,7 @@ describe('loadBlog', () => {
|
|||
source: path.posix.join('@site', PluginPath, 'complex-slug.md'),
|
||||
title: 'Complex Slug',
|
||||
description: `complex url slug`,
|
||||
authors: [],
|
||||
prevItem: undefined,
|
||||
nextItem: {
|
||||
permalink: '/blog/simple/slug',
|
||||
|
@ -169,6 +203,14 @@ describe('loadBlog', () => {
|
|||
source: path.posix.join('@site', PluginPath, 'simple-slug.md'),
|
||||
title: 'Simple Slug',
|
||||
description: `simple url slug`,
|
||||
authors: [
|
||||
{
|
||||
name: 'Sébastien Lorber',
|
||||
title: 'Docusaurus maintainer',
|
||||
url: 'https://sebastienlorber.com',
|
||||
imageURL: undefined,
|
||||
},
|
||||
],
|
||||
prevItem: undefined,
|
||||
nextItem: {
|
||||
permalink: '/blog/draft',
|
||||
|
@ -190,6 +232,7 @@ describe('loadBlog', () => {
|
|||
source: path.posix.join('@site', PluginPath, 'heading-as-title.md'),
|
||||
title: 'some heading',
|
||||
description: '',
|
||||
authors: [],
|
||||
date: new Date('2019-01-02'),
|
||||
formattedDate: 'January 2, 2019',
|
||||
prevItem: undefined,
|
||||
|
@ -325,6 +368,7 @@ describe('loadBlog', () => {
|
|||
source: noDateSource,
|
||||
title: 'no date',
|
||||
description: `no date`,
|
||||
authors: [],
|
||||
date: noDateSourceBirthTime,
|
||||
formattedDate,
|
||||
tags: [],
|
||||
|
|
202
packages/docusaurus-plugin-content-blog/src/authors.ts
Normal file
202
packages/docusaurus-plugin-content-blog/src/authors.ts
Normal file
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* 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 fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import {Author, BlogContentPaths} from './types';
|
||||
import {findFolderContainingFile} from '@docusaurus/utils';
|
||||
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
||||
import {
|
||||
BlogPostFrontMatter,
|
||||
BlogPostFrontMatterAuthor,
|
||||
BlogPostFrontMatterAuthors,
|
||||
} from './blogFrontMatter';
|
||||
import {getContentPathList} from './blogUtils';
|
||||
import Yaml from 'js-yaml';
|
||||
|
||||
export type AuthorsMap = Record<string, Author>;
|
||||
|
||||
const AuthorsMapSchema = Joi.object<AuthorsMap>().pattern(
|
||||
Joi.string(),
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
url: URISchema,
|
||||
imageURL: URISchema,
|
||||
title: Joi.string(),
|
||||
})
|
||||
.rename('image_url', 'imageURL')
|
||||
.unknown()
|
||||
.required(),
|
||||
);
|
||||
|
||||
export function validateAuthorsMapFile(content: unknown): AuthorsMap {
|
||||
return Joi.attempt(content, AuthorsMapSchema);
|
||||
}
|
||||
|
||||
export async function readAuthorsMapFile(
|
||||
filePath: string,
|
||||
): Promise<AuthorsMap | undefined> {
|
||||
if (await fs.pathExists(filePath)) {
|
||||
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
|
||||
const parse =
|
||||
filePath.endsWith('.yml') || filePath.endsWith('.yaml')
|
||||
? Yaml.load
|
||||
: JSON.parse;
|
||||
try {
|
||||
const unsafeContent = parse(contentString);
|
||||
return validateAuthorsMapFile(unsafeContent);
|
||||
} catch (e) {
|
||||
// TODO replace later by error cause: see https://v8.dev/features/error-cause
|
||||
console.error(chalk.red('The author list file looks invalid!'));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type AuthorsMapParams = {
|
||||
authorsMapPath: string;
|
||||
contentPaths: BlogContentPaths;
|
||||
};
|
||||
|
||||
export async function getAuthorsMapFilePath({
|
||||
authorsMapPath,
|
||||
contentPaths,
|
||||
}: AuthorsMapParams): Promise<string | undefined> {
|
||||
// Useful to load an eventually localize authors map
|
||||
const contentPath = await findFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
authorsMapPath,
|
||||
);
|
||||
|
||||
if (contentPath) {
|
||||
return path.join(contentPath, authorsMapPath);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getAuthorsMap(
|
||||
params: AuthorsMapParams,
|
||||
): Promise<AuthorsMap | undefined> {
|
||||
const filePath = await getAuthorsMapFilePath(params);
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await readAuthorsMapFile(filePath);
|
||||
} catch (e) {
|
||||
// TODO replace later by error cause, see https://v8.dev/features/error-cause
|
||||
console.error(
|
||||
chalk.red(`Couldn't read blog authors map at path ${filePath}`),
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
type AuthorsParam = {
|
||||
frontMatter: BlogPostFrontMatter;
|
||||
authorsMap: AuthorsMap | undefined;
|
||||
};
|
||||
|
||||
// Legacy v1/early-v2 frontmatter fields
|
||||
// We may want to deprecate those in favor of using only frontMatter.authors
|
||||
function getFrontMatterAuthorLegacy(
|
||||
frontMatter: BlogPostFrontMatter,
|
||||
): BlogPostFrontMatterAuthor | undefined {
|
||||
const name = frontMatter.author;
|
||||
const title = frontMatter.author_title ?? frontMatter.authorTitle;
|
||||
const url = frontMatter.author_url ?? frontMatter.authorURL;
|
||||
const imageURL = frontMatter.author_image_url ?? frontMatter.authorImageURL;
|
||||
|
||||
// Shouldn't we require at least an author name?
|
||||
if (name || title || url || imageURL) {
|
||||
return {
|
||||
name,
|
||||
title,
|
||||
url,
|
||||
imageURL,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeFrontMatterAuthors(
|
||||
frontMatterAuthors: BlogPostFrontMatterAuthors = [],
|
||||
): BlogPostFrontMatterAuthor[] {
|
||||
function normalizeAuthor(
|
||||
authorInput: string | BlogPostFrontMatterAuthor,
|
||||
): BlogPostFrontMatterAuthor {
|
||||
if (typeof authorInput === 'string') {
|
||||
// Technically, we could allow users to provide an author's name here
|
||||
// IMHO it's better to only support keys here
|
||||
// Reason: a typo in a key would fallback to becoming a name and may end-up un-noticed
|
||||
return {key: authorInput};
|
||||
}
|
||||
return authorInput;
|
||||
}
|
||||
|
||||
return Array.isArray(frontMatterAuthors)
|
||||
? frontMatterAuthors.map(normalizeAuthor)
|
||||
: [normalizeAuthor(frontMatterAuthors)];
|
||||
}
|
||||
|
||||
function getFrontMatterAuthors(params: AuthorsParam): Author[] {
|
||||
const {authorsMap} = params;
|
||||
const frontMatterAuthors = normalizeFrontMatterAuthors(
|
||||
params.frontMatter.authors,
|
||||
);
|
||||
|
||||
function getAuthorsMapAuthor(key: string | undefined): Author | undefined {
|
||||
if (key) {
|
||||
if (!authorsMap || Object.keys(authorsMap).length === 0) {
|
||||
throw new Error(`Can't reference blog post authors by a key (such as '${key}') 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!`);
|
||||
}
|
||||
const author = authorsMap[key];
|
||||
if (!author) {
|
||||
throw Error(`Blog author with key "${key}" not found in the authors map file.
|
||||
Valid author keys are:
|
||||
${Object.keys(authorsMap)
|
||||
.map((validKey) => `- ${validKey}`)
|
||||
.join('\n')}`);
|
||||
}
|
||||
return author;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
|
||||
return {
|
||||
// Author def from authorsMap can be locally overridden by frontmatter
|
||||
...getAuthorsMapAuthor(frontMatterAuthor.key),
|
||||
...frontMatterAuthor,
|
||||
};
|
||||
}
|
||||
|
||||
return frontMatterAuthors.map(toAuthor);
|
||||
}
|
||||
|
||||
export function getBlogPostAuthors(params: AuthorsParam): Author[] {
|
||||
const authorLegacy = getFrontMatterAuthorLegacy(params.frontMatter);
|
||||
const authors = getFrontMatterAuthors(params);
|
||||
|
||||
if (authorLegacy) {
|
||||
// Technically, we could allow mixing legacy/authors frontmatter, but do we really want to?
|
||||
if (authors.length > 0) {
|
||||
throw new Error(
|
||||
`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.`,
|
||||
);
|
||||
}
|
||||
return [authorLegacy];
|
||||
}
|
||||
|
||||
return authors;
|
||||
}
|
|
@ -13,6 +13,30 @@ import {
|
|||
} from '@docusaurus/utils-validation';
|
||||
import type {FrontMatterTag} from '@docusaurus/utils';
|
||||
|
||||
export type BlogPostFrontMatterAuthor = Record<string, unknown> & {
|
||||
key?: string;
|
||||
name?: string;
|
||||
imageURL?: string;
|
||||
url?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
// All the possible variants that the user can use for convenience
|
||||
export type BlogPostFrontMatterAuthors =
|
||||
| string
|
||||
| BlogPostFrontMatterAuthor
|
||||
| (string | BlogPostFrontMatterAuthor)[];
|
||||
|
||||
const BlogPostFrontMatterAuthorSchema = Joi.object({
|
||||
key: Joi.string(),
|
||||
name: Joi.string(),
|
||||
title: Joi.string(),
|
||||
url: URISchema,
|
||||
imageURL: Joi.string(),
|
||||
})
|
||||
.or('key', 'name')
|
||||
.rename('image_url', 'imageURL', {alias: true});
|
||||
|
||||
export type BlogPostFrontMatter = {
|
||||
/* eslint-disable camelcase */
|
||||
id?: string;
|
||||
|
@ -23,22 +47,30 @@ export type BlogPostFrontMatter = {
|
|||
draft?: boolean;
|
||||
date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
|
||||
|
||||
authors?: BlogPostFrontMatterAuthors;
|
||||
|
||||
// We may want to deprecate those older author frontmatter fields later:
|
||||
author?: string;
|
||||
author_title?: string;
|
||||
author_url?: string;
|
||||
author_image_url?: string;
|
||||
|
||||
/** @deprecated */
|
||||
authorTitle?: string;
|
||||
/** @deprecated */
|
||||
authorURL?: string;
|
||||
/** @deprecated */
|
||||
authorImageURL?: string;
|
||||
|
||||
image?: string;
|
||||
keywords?: string[];
|
||||
hide_table_of_contents?: boolean;
|
||||
|
||||
/** @deprecated */
|
||||
authorTitle?: string;
|
||||
authorURL?: string;
|
||||
authorImageURL?: string;
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
|
||||
const FrontMatterAuthorErrorMessage =
|
||||
'{{#label}} does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).';
|
||||
|
||||
const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
|
||||
id: Joi.string(),
|
||||
title: Joi.string().allow(''),
|
||||
|
@ -47,28 +79,42 @@ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
|
|||
draft: Joi.boolean(),
|
||||
date: Joi.date().raw(),
|
||||
|
||||
// New multi-authors frontmatter:
|
||||
authors: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string(),
|
||||
BlogPostFrontMatterAuthorSchema,
|
||||
Joi.array()
|
||||
.items(Joi.string(), BlogPostFrontMatterAuthorSchema)
|
||||
.messages({
|
||||
'array.sparse': FrontMatterAuthorErrorMessage,
|
||||
'array.includes': FrontMatterAuthorErrorMessage,
|
||||
}),
|
||||
)
|
||||
.messages({
|
||||
'alternatives.match': FrontMatterAuthorErrorMessage,
|
||||
}),
|
||||
// Legacy author frontmatter
|
||||
author: Joi.string(),
|
||||
author_title: Joi.string(),
|
||||
author_url: URISchema,
|
||||
author_image_url: URISchema,
|
||||
slug: Joi.string(),
|
||||
image: URISchema,
|
||||
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
|
||||
// TODO enable deprecation warnings later
|
||||
authorURL: URISchema,
|
||||
// .warning('deprecate.error', { alternative: '"author_url"'}),
|
||||
authorTitle: Joi.string(),
|
||||
// .warning('deprecate.error', { alternative: '"author_title"'}),
|
||||
authorImageURL: URISchema,
|
||||
// .warning('deprecate.error', { alternative: '"author_image_url"'}),
|
||||
})
|
||||
.unknown()
|
||||
.messages({
|
||||
'deprecate.error':
|
||||
'{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
|
||||
});
|
||||
|
||||
slug: Joi.string(),
|
||||
image: URISchema,
|
||||
keywords: Joi.array().items(Joi.string().required()),
|
||||
hide_table_of_contents: Joi.boolean(),
|
||||
}).messages({
|
||||
'deprecate.error':
|
||||
'{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
|
||||
});
|
||||
|
||||
export function validateBlogPostFrontMatter(
|
||||
frontMatter: Record<string, unknown>,
|
||||
|
|
|
@ -9,7 +9,7 @@ import fs from 'fs-extra';
|
|||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import readingTime from 'reading-time';
|
||||
import {Feed} from 'feed';
|
||||
import {Feed, Author as FeedAuthor} from 'feed';
|
||||
import {compact, keyBy, mapValues} from 'lodash';
|
||||
import {
|
||||
PluginOptions,
|
||||
|
@ -17,6 +17,7 @@ import {
|
|||
BlogContentPaths,
|
||||
BlogMarkdownLoaderOptions,
|
||||
BlogTags,
|
||||
Author,
|
||||
} from './types';
|
||||
import {
|
||||
parseMarkdownFile,
|
||||
|
@ -32,6 +33,7 @@ import {
|
|||
} from '@docusaurus/utils';
|
||||
import {LoadContext} from '@docusaurus/types';
|
||||
import {validateBlogPostFrontMatter} from './blogFrontMatter';
|
||||
import {AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
|
||||
|
||||
export function truncate(fileString: string, truncateMarker: RegExp): string {
|
||||
return fileString.split(truncateMarker, 1).shift()!;
|
||||
|
@ -135,10 +137,16 @@ export async function generateBlogFeed(
|
|||
copyright: feedOptions.copyright,
|
||||
});
|
||||
|
||||
function toFeedAuthor(author: Author): FeedAuthor {
|
||||
// TODO ask author emails?
|
||||
// RSS feed requires email to render authors
|
||||
return {name: author.name, link: author.url};
|
||||
}
|
||||
|
||||
blogPosts.forEach((post) => {
|
||||
const {
|
||||
id,
|
||||
metadata: {title: metadataTitle, permalink, date, description},
|
||||
metadata: {title: metadataTitle, permalink, date, description, authors},
|
||||
} = post;
|
||||
feed.addItem({
|
||||
title: metadataTitle,
|
||||
|
@ -146,6 +154,7 @@ export async function generateBlogFeed(
|
|||
link: normalizeUrl([siteUrl, permalink]),
|
||||
date,
|
||||
description,
|
||||
author: authors.map(toFeedAuthor),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -167,6 +176,7 @@ async function processBlogSourceFile(
|
|||
contentPaths: BlogContentPaths,
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
authorsMap?: AuthorsMap,
|
||||
): Promise<BlogPost | undefined> {
|
||||
const {
|
||||
siteConfig: {baseUrl},
|
||||
|
@ -258,6 +268,7 @@ async function processBlogSourceFile(
|
|||
}
|
||||
|
||||
const tagsBasePath = normalizeUrl([baseUrl, options.routeBasePath, 'tags']); // make this configurable?
|
||||
const authors = getBlogPostAuthors({authorsMap, frontMatter});
|
||||
|
||||
return {
|
||||
id: frontMatter.slug ?? title,
|
||||
|
@ -272,6 +283,7 @@ async function processBlogSourceFile(
|
|||
tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
|
||||
readingTime: showReadingTime ? readingTime(content).minutes : undefined,
|
||||
truncated: truncateMarker?.test(content) || false,
|
||||
authors,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -292,6 +304,11 @@ export async function generateBlogPosts(
|
|||
ignore: exclude,
|
||||
});
|
||||
|
||||
const authorsMap = await getAuthorsMap({
|
||||
contentPaths,
|
||||
authorsMapPath: options.authorsMapPath,
|
||||
});
|
||||
|
||||
const blogPosts: BlogPost[] = compact(
|
||||
await Promise.all(
|
||||
blogSourceFiles.map(async (blogSourceFile: string) => {
|
||||
|
@ -301,6 +318,7 @@ export async function generateBlogPosts(
|
|||
contentPaths,
|
||||
context,
|
||||
options,
|
||||
authorsMap,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
|
|
|
@ -32,9 +32,10 @@ import {
|
|||
BlogItemsToMetadata,
|
||||
TagsModule,
|
||||
BlogPaginated,
|
||||
BlogPost,
|
||||
BlogContentPaths,
|
||||
BlogMarkdownLoaderOptions,
|
||||
MetaData,
|
||||
Assets,
|
||||
} from './types';
|
||||
import {PluginOptionSchema} from './pluginOptionSchema';
|
||||
import {
|
||||
|
@ -54,6 +55,7 @@ import {
|
|||
getSourceToPermalink,
|
||||
getBlogTags,
|
||||
} from './blogUtils';
|
||||
import {BlogPostFrontMatter} from './blogFrontMatter';
|
||||
|
||||
export default function pluginContentBlog(
|
||||
context: LoadContext,
|
||||
|
@ -95,12 +97,22 @@ export default function pluginContentBlog(
|
|||
name: 'docusaurus-plugin-content-blog',
|
||||
|
||||
getPathsToWatch() {
|
||||
const {include = []} = options;
|
||||
return flatten(
|
||||
const {include, authorsMapPath} = options;
|
||||
const contentMarkdownGlobs = flatten(
|
||||
getContentPathList(contentPaths).map((contentPath) => {
|
||||
return include.map((pattern) => `${contentPath}/${pattern}`);
|
||||
}),
|
||||
);
|
||||
|
||||
// TODO: we should read this path in plugin! but plugins do not support async init for now :'(
|
||||
// const authorsMapFilePath = await getAuthorsMapFilePath({authorsMapPath,contentPaths,});
|
||||
// simplified impl, better than nothing for now:
|
||||
const authorsMapFilePath = path.join(
|
||||
contentPaths.contentPath,
|
||||
authorsMapPath,
|
||||
);
|
||||
|
||||
return [authorsMapFilePath, ...contentMarkdownGlobs];
|
||||
},
|
||||
|
||||
async getTranslationFiles() {
|
||||
|
@ -117,11 +129,7 @@ export default function pluginContentBlog(
|
|||
blogSidebarTitle,
|
||||
} = options;
|
||||
|
||||
const blogPosts: BlogPost[] = await generateBlogPosts(
|
||||
contentPaths,
|
||||
context,
|
||||
options,
|
||||
);
|
||||
const blogPosts = await generateBlogPosts(contentPaths, context, options);
|
||||
|
||||
if (!blogPosts.length) {
|
||||
return {
|
||||
|
@ -454,12 +462,22 @@ export default function pluginContentBlog(
|
|||
// For blog posts a title in markdown is always removed
|
||||
// Blog posts title are rendered separately
|
||||
removeContentTitle: true,
|
||||
// those frontMatter fields will be exported as "frontMatterAssets" and eventually be converted to require() calls for relative file paths
|
||||
frontMatterAssetKeys: [
|
||||
'image',
|
||||
'authorImageURL',
|
||||
'author_image_URL',
|
||||
],
|
||||
|
||||
// Assets allow to convert some relative images paths to require() calls
|
||||
createAssets: ({
|
||||
frontMatter,
|
||||
metadata,
|
||||
}: {
|
||||
frontMatter: BlogPostFrontMatter;
|
||||
metadata: MetaData;
|
||||
}): Assets => {
|
||||
return {
|
||||
image: frontMatter.image,
|
||||
authorsImageUrls: metadata.authors.map(
|
||||
(author) => author.imageURL,
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -38,6 +38,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
|
|||
routeBasePath: 'blog',
|
||||
path: 'blog',
|
||||
editLocalizedFiles: false,
|
||||
authorsMapPath: 'authors.yml',
|
||||
};
|
||||
|
||||
export const PluginOptionSchema = Joi.object<PluginOptions>({
|
||||
|
@ -107,4 +108,5 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
|
|||
}),
|
||||
language: Joi.string(),
|
||||
}).default(DEFAULT_OPTIONS.feedOptions),
|
||||
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
|
||||
});
|
||||
|
|
|
@ -58,6 +58,7 @@ export interface PluginOptions extends RemarkAndRehypePluginOptions {
|
|||
editUrl?: string | EditUrlFunction;
|
||||
editLocalizedFiles?: boolean;
|
||||
admonitions: Record<string, unknown>;
|
||||
authorsMapPath: string;
|
||||
}
|
||||
|
||||
export interface BlogTags {
|
||||
|
@ -92,6 +93,14 @@ export interface BlogPaginated {
|
|||
items: string[];
|
||||
}
|
||||
|
||||
// We allow passing custom fields to authors, e.g., twitter
|
||||
export interface Author extends Record<string, unknown> {
|
||||
name?: string;
|
||||
imageURL?: string;
|
||||
url?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface MetaData {
|
||||
permalink: string;
|
||||
source: string;
|
||||
|
@ -105,6 +114,12 @@ export interface MetaData {
|
|||
nextItem?: Paginator;
|
||||
truncated: boolean;
|
||||
editUrl?: string;
|
||||
authors: Author[];
|
||||
}
|
||||
|
||||
export interface Assets {
|
||||
image?: string;
|
||||
authorsImageUrls: (string | undefined)[]; // Array of same size as the original MetaData.authors array
|
||||
}
|
||||
|
||||
export interface Paginator {
|
||||
|
|
|
@ -151,7 +151,7 @@ describe('validateDocFrontMatter image', () => {
|
|||
{image: '../relative/image.png'},
|
||||
],
|
||||
invalidFrontMatters: [
|
||||
[{image: ''}, 'does not match any of the allowed types'],
|
||||
[{image: ''}, '"image" does not look like a valid url (value=\'\')'],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,11 +40,7 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
} = props;
|
||||
|
||||
const {date, readingTime, tags, permalink, editUrl} = metadata;
|
||||
const {author, title} = frontMatter;
|
||||
|
||||
const authorURL = frontMatter.author_url || frontMatter.authorURL;
|
||||
const authorImageURL =
|
||||
frontMatter.author_image_url || frontMatter.authorImageURL;
|
||||
const {author, title, authorURL, authorImageURL} = frontMatter;
|
||||
|
||||
const match = date.substring(0, 10).split('-');
|
||||
const year = match[0];
|
||||
|
@ -56,13 +52,17 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
<div className="row no-gutters rows-col-2 m-3">
|
||||
<div className="col-xs mr-3">
|
||||
{authorImageURL && (
|
||||
<img style={{width: '50px'}} src={authorImageURL} alt={author} />
|
||||
<img
|
||||
style={{width: '50px'}}
|
||||
src={authorImageURL}
|
||||
alt={author as string}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col">
|
||||
{author && (
|
||||
<h5>
|
||||
<a href={authorURL} rel={author}>
|
||||
<a href={authorURL} rel={author as string}>
|
||||
{author}
|
||||
</a>
|
||||
</h5>
|
||||
|
|
|
@ -38,7 +38,7 @@ function BlogListPage(props: Props): JSX.Element {
|
|||
<BlogPostItem
|
||||
key={BlogPostContent.metadata.permalink}
|
||||
frontMatter={BlogPostContent.frontMatter}
|
||||
frontMatterAssets={BlogPostContent.frontMatterAssets}
|
||||
assets={BlogPostContent.assets}
|
||||
metadata={BlogPostContent.metadata}
|
||||
truncated={BlogPostContent.metadata.truncated}>
|
||||
<BlogPostContent />
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import type {Props} from '@theme/BlogPostAuthor';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function BlogPostAuthor({author}: Props): JSX.Element {
|
||||
const {name, title, url, imageURL} = author;
|
||||
return (
|
||||
<div className="avatar margin-bottom--sm">
|
||||
{imageURL && (
|
||||
<Link className="avatar__photo-link avatar__photo" href={url}>
|
||||
<img className={styles.image} src={imageURL} alt={name} />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{
|
||||
// Note: only legacy author frontmatter allow empty name (not frontMatter.authors)
|
||||
name && (
|
||||
<div
|
||||
className="avatar__intro"
|
||||
itemProp="author"
|
||||
itemScope
|
||||
itemType="https://schema.org/Person">
|
||||
<div className="avatar__name">
|
||||
<Link href={url} itemProp="url">
|
||||
<span itemProp="name">{name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
{title && (
|
||||
<small className="avatar__subtitle" itemProp="description">
|
||||
{title}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlogPostAuthor;
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 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 React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import type {Props} from '@theme/BlogPostAuthors';
|
||||
import BlogPostAuthor from '@theme/BlogPostAuthor';
|
||||
|
||||
function getAuthorsPerLine(authorsCount: number): 1 | 2 {
|
||||
switch (authorsCount) {
|
||||
case 0:
|
||||
case 1:
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
function getColClassName(authorsCount: number): string {
|
||||
switch (getAuthorsPerLine(authorsCount)) {
|
||||
case 1:
|
||||
return 'col--12';
|
||||
case 2:
|
||||
return 'col--6';
|
||||
default:
|
||||
throw Error('unexpected');
|
||||
}
|
||||
}
|
||||
|
||||
// Component responsible for the authors layout
|
||||
export default function BlogPostAuthors({authors, assets}: Props): JSX.Element {
|
||||
const authorsCount = authors.length;
|
||||
if (authorsCount === 0) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<div className="row margin-top--md margin-bottom--sm">
|
||||
{authors.map((author, idx) => (
|
||||
<div className={clsx('col', getColClassName(authorsCount))} key={idx}>
|
||||
<BlogPostAuthor
|
||||
author={{
|
||||
...author,
|
||||
// Handle author images using relative paths
|
||||
imageURL: assets.authorsImageUrls[idx] ?? author.imageURL,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -18,6 +18,7 @@ import type {Props} from '@theme/BlogPostItem';
|
|||
|
||||
import styles from './styles.module.css';
|
||||
import TagsListInline from '@theme/TagsListInline';
|
||||
import BlogPostAuthors from '@theme/BlogPostAuthors';
|
||||
|
||||
// Very simple pluralization: probably good enough for now
|
||||
function useReadingTimePlural() {
|
||||
|
@ -45,7 +46,7 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
const {
|
||||
children,
|
||||
frontMatter,
|
||||
frontMatterAssets,
|
||||
assets,
|
||||
metadata,
|
||||
truncated,
|
||||
isBlogPostPage = false,
|
||||
|
@ -58,18 +59,10 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
readingTime,
|
||||
title,
|
||||
editUrl,
|
||||
authors,
|
||||
} = metadata;
|
||||
const {author} = frontMatter;
|
||||
|
||||
const image = frontMatterAssets.image ?? frontMatter.image;
|
||||
|
||||
const authorURL = frontMatter.author_url || frontMatter.authorURL;
|
||||
const authorTitle = frontMatter.author_title || frontMatter.authorTitle;
|
||||
const authorImageURL =
|
||||
frontMatterAssets.author_image_url ||
|
||||
frontMatterAssets.authorImageURL ||
|
||||
frontMatter.author_image_url ||
|
||||
frontMatter.authorImageURL;
|
||||
const image = assets.image ?? frontMatter.image;
|
||||
|
||||
const renderPostHeader = () => {
|
||||
const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
|
||||
|
@ -90,39 +83,14 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
{formattedDate}
|
||||
</time>
|
||||
|
||||
{readingTime && (
|
||||
{typeof readingTime !== 'undefined' && (
|
||||
<>
|
||||
{' · '}
|
||||
{readingTimePlural(readingTime)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="avatar margin-vert--md">
|
||||
{authorImageURL && (
|
||||
<Link className="avatar__photo-link avatar__photo" href={authorURL}>
|
||||
<img src={authorImageURL} alt={author} />
|
||||
</Link>
|
||||
)}
|
||||
{author && (
|
||||
<div
|
||||
className="avatar__intro"
|
||||
itemProp="author"
|
||||
itemScope
|
||||
itemType="https://schema.org/Person">
|
||||
<div className="avatar__name">
|
||||
<Link href={authorURL} itemProp="url">
|
||||
<span itemProp="name">{author}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{authorTitle && (
|
||||
<small className="avatar__subtitle" itemProp="description">
|
||||
{authorTitle}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BlogPostAuthors authors={authors} assets={assets} />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,12 +15,19 @@ import {ThemeClassNames} from '@docusaurus/theme-common';
|
|||
|
||||
function BlogPostPage(props: Props): JSX.Element {
|
||||
const {content: BlogPostContents, sidebar} = props;
|
||||
const {frontMatter, frontMatterAssets, metadata} = BlogPostContents;
|
||||
const {title, description, nextItem, prevItem, date, tags} = metadata;
|
||||
const {frontMatter, assets, metadata} = BlogPostContents;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
nextItem,
|
||||
prevItem,
|
||||
date,
|
||||
tags,
|
||||
authors,
|
||||
} = metadata;
|
||||
const {hide_table_of_contents: hideTableOfContents, keywords} = frontMatter;
|
||||
|
||||
const image = frontMatterAssets.image ?? frontMatter.image;
|
||||
const authorURL = frontMatter.author_url || frontMatter.authorURL;
|
||||
const image = assets.image ?? frontMatter.image;
|
||||
|
||||
return (
|
||||
<BlogLayout
|
||||
|
@ -41,7 +48,17 @@ function BlogPostPage(props: Props): JSX.Element {
|
|||
image={image}>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="article:published_time" content={date} />
|
||||
{authorURL && <meta property="article:author" content={authorURL} />}
|
||||
|
||||
{/* TODO double check those article metas array syntaxes, see https://ogp.me/#array */}
|
||||
{authors.some((author) => author.url) && (
|
||||
<meta
|
||||
property="article:author"
|
||||
content={authors
|
||||
.map((author) => author.url)
|
||||
.filter(Boolean)
|
||||
.join(',')}
|
||||
/>
|
||||
)}
|
||||
{tags.length > 0 && (
|
||||
<meta
|
||||
property="article:tag"
|
||||
|
@ -52,7 +69,7 @@ function BlogPostPage(props: Props): JSX.Element {
|
|||
|
||||
<BlogPostItem
|
||||
frontMatter={frontMatter}
|
||||
frontMatterAssets={frontMatterAssets}
|
||||
assets={assets}
|
||||
metadata={metadata}
|
||||
isBlogPostPage>
|
||||
<BlogPostContents />
|
||||
|
|
|
@ -71,7 +71,7 @@ function BlogTagsPostPage(props: Props): JSX.Element {
|
|||
<BlogPostItem
|
||||
key={BlogPostContent.metadata.permalink}
|
||||
frontMatter={BlogPostContent.frontMatter}
|
||||
frontMatterAssets={BlogPostContent.frontMatterAssets}
|
||||
assets={BlogPostContent.assets}
|
||||
metadata={BlogPostContent.metadata}
|
||||
truncated>
|
||||
<BlogPostContent />
|
||||
|
|
29
packages/docusaurus-theme-classic/src/types.d.ts
vendored
29
packages/docusaurus-theme-classic/src/types.d.ts
vendored
|
@ -27,15 +27,11 @@ declare module '@theme/BlogListPaginator' {
|
|||
}
|
||||
|
||||
declare module '@theme/BlogPostItem' {
|
||||
import type {
|
||||
FrontMatter,
|
||||
FrontMatterAssets,
|
||||
Metadata,
|
||||
} from '@theme/BlogPostPage';
|
||||
import type {FrontMatter, Assets, Metadata} from '@theme/BlogPostPage';
|
||||
|
||||
export type Props = {
|
||||
readonly frontMatter: FrontMatter;
|
||||
readonly frontMatterAssets: FrontMatterAssets;
|
||||
readonly assets: Assets;
|
||||
readonly metadata: Metadata;
|
||||
readonly truncated?: string | boolean;
|
||||
readonly isBlogPostPage?: boolean;
|
||||
|
@ -46,6 +42,27 @@ declare module '@theme/BlogPostItem' {
|
|||
export default BlogPostItem;
|
||||
}
|
||||
|
||||
declare module '@theme/BlogPostAuthor' {
|
||||
import type {Metadata} from '@theme/BlogPostPage';
|
||||
|
||||
export type Props = {
|
||||
readonly author: Metadata['authors'][number];
|
||||
};
|
||||
|
||||
export default function BlogPostAuthor(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/BlogPostAuthors' {
|
||||
import type {Metadata, Assets} from '@theme/BlogPostPage';
|
||||
|
||||
export type Props = {
|
||||
readonly authors: Metadata['authors'];
|
||||
readonly assets: Assets;
|
||||
};
|
||||
|
||||
export default function BlogPostAuthors(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/BlogPostPaginator' {
|
||||
type Item = {readonly title: string; readonly permalink: string};
|
||||
|
||||
|
|
|
@ -60,4 +60,4 @@ exports[`validation schemas RemarkPluginsSchema: for value=false 1`] = `"\\"valu
|
|||
|
||||
exports[`validation schemas RemarkPluginsSchema: for value=null 1`] = `"\\"value\\" must be an array"`;
|
||||
|
||||
exports[`validation schemas URISchema: for value="spaces are invalid in a URL" 1`] = `"\\"value\\" does not match any of the allowed types"`;
|
||||
exports[`validation schemas URISchema: for value="spaces are invalid in a URL" 1`] = `"\\"value\\" does not look like a valid url (value='')"`;
|
||||
|
|
|
@ -44,7 +44,10 @@ export const URISchema = Joi.alternatives(
|
|||
return helpers.error('any.invalid');
|
||||
}
|
||||
}),
|
||||
);
|
||||
).messages({
|
||||
'alternatives.match':
|
||||
"{{#label}} does not look like a valid url (value='{{.value}}')",
|
||||
});
|
||||
|
||||
export const PathnameSchema = Joi.string()
|
||||
.custom((val) => {
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
---
|
||||
title: Using twice the blog plugin
|
||||
author: Sebastien Lorber
|
||||
authorURL: https://sebastienlorber.com
|
||||
authorImageURL: https://github.com/slorber.png
|
||||
authorFBID: 611217057
|
||||
authorTwitter: sebastienlorber
|
||||
authors: [slorber]
|
||||
tags: [blog, docusaurus]
|
||||
---
|
||||
|
||||
|
|
3
website/_dogfooding/_blog tests/2021-08-22-no-author.md
Normal file
3
website/_dogfooding/_blog tests/2021-08-22-no-author.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Hmmm!
|
||||
|
||||
This is a blog post from an anonymous author!
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
authors:
|
||||
- slorber
|
||||
- name: Josh-Cena
|
||||
image_url: https://avatars.githubusercontent.com/u/55398995?v=4
|
||||
url: https://joshcena.com
|
||||
---
|
||||
|
||||
# Multiple authors
|
||||
|
||||
You can have multiple authors for one blog post!
|
6
website/_dogfooding/_blog tests/authors.yml
Normal file
6
website/_dogfooding/_blog tests/authors.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
slorber:
|
||||
name: Sebastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
twitter: sebastienlorber
|
|
@ -1,10 +1,6 @@
|
|||
---
|
||||
title: Introducing Docusaurus
|
||||
author: Joel Marcey
|
||||
authorURL: http://twitter.com/JoelMarcey
|
||||
authorImageURL: https://graph.facebook.com/611217057/picture/?height=200&width=200
|
||||
authorFBID: 611217057
|
||||
authorTwitter: JoelMarcey
|
||||
authors: JMarcey
|
||||
tags: [documentation, blog, docusaurus]
|
||||
---
|
||||
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
title: How I Converted Profilo to Docusaurus in Under 2 Hours
|
||||
author: Christine Abernathy
|
||||
authorURL: http://twitter.com/abernathyca
|
||||
authorImageURL: https://graph.facebook.com/1424840234/picture/?height=200&width=200
|
||||
authorFBID: 1424840234
|
||||
authorImageURL: https://github.com/caabernathy.png
|
||||
authorTwitter: abernathyca
|
||||
tags: [profilo, adoption]
|
||||
---
|
||||
|
|
|
@ -3,7 +3,7 @@ title: Towards Docusaurus 2
|
|||
author: Endilie Yacop Sucipto
|
||||
authorTitle: Maintainer of Docusaurus
|
||||
authorURL: https://github.com/endiliey
|
||||
authorImageURL: https://avatars1.githubusercontent.com/u/17883920?s=460&v=4
|
||||
authorImageURL: https://github.com/endiliey.png
|
||||
authorTwitter: endiliey
|
||||
tags: [new, adoption]
|
||||
---
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
---
|
||||
title: Happy 1st Birthday Slash!
|
||||
author: Joel Marcey
|
||||
authorTitle: Co-creator of Docusaurus
|
||||
authorURL: https://github.com/JoelMarcey
|
||||
authorImageURL: https://graph.facebook.com/611217057/picture/?height=200&width=200
|
||||
authorFBID: 611217057
|
||||
authorTwitter: JoelMarcey
|
||||
authors:
|
||||
- key: JMarcey
|
||||
title: Co-creator of Docusaurus
|
||||
tags: [birth]
|
||||
---
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
---
|
||||
title: Docusaurus 2019 Recap
|
||||
author: Yangshun Tay
|
||||
authorTitle: Front End Engineer at Facebook
|
||||
authorURL: https://github.com/yangshun
|
||||
authorImageURL: https://avatars1.githubusercontent.com/u/1315101?s=460&v=4
|
||||
authorTwitter: yangshunz
|
||||
authors: yangshun
|
||||
tags: [recap]
|
||||
---
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
---
|
||||
title: Tribute to Endi
|
||||
author: Joel Marcey
|
||||
authorTitle: Technical Lead and Developer Advocate at Facebook
|
||||
authorURL: https://github.com/JoelMarcey
|
||||
authorImageURL: https://graph.facebook.com/611217057/picture/?height=200&width=200
|
||||
authorTwitter: JoelMarcey
|
||||
authors: JMarcey
|
||||
tags: [endi, tribute]
|
||||
---
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
---
|
||||
title: Docusaurus 2020 Recap
|
||||
author: Sébastien Lorber
|
||||
authorTitle: Docusaurus maintainer
|
||||
authorURL: https://sebastienlorber.com
|
||||
authorImageURL: https://github.com/slorber.png
|
||||
authorTwitter: sebastienlorber
|
||||
authors: [slorber]
|
||||
tags: [recap]
|
||||
image: /img/docusaurus-2020-recap.png
|
||||
---
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
---
|
||||
title: Releasing Docusaurus i18n
|
||||
author: Sébastien Lorber
|
||||
authorTitle: Docusaurus maintainer
|
||||
authorURL: https://sebastienlorber.com
|
||||
authorImageURL: https://github.com/slorber.png
|
||||
authorTwitter: sebastienlorber
|
||||
authors: [slorber]
|
||||
tags: [release, i18n]
|
||||
image: /img/blog/2021-03-09-releasing-docusaurus-i18n/social-card.png
|
||||
---
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
|
@ -1,10 +1,12 @@
|
|||
---
|
||||
title: Announcing Docusaurus 2 Beta
|
||||
author: Sébastien Lorber
|
||||
authorTitle: Docusaurus maintainer
|
||||
authorURL: https://sebastienlorber.com
|
||||
authorImageURL: ./img/author.jpeg
|
||||
authorTwitter: sebastienlorber
|
||||
authors:
|
||||
- key: slorber
|
||||
image_url: ./img/slorber.png
|
||||
- JMarcey
|
||||
- yangshun
|
||||
- lex111
|
||||
|
||||
tags: [release, beta]
|
||||
image: ./img/social-card.png
|
||||
---
|
||||
|
|
26
website/blog/authors.yml
Normal file
26
website/blog/authors.yml
Normal file
|
@ -0,0 +1,26 @@
|
|||
JMarcey:
|
||||
name: Joel Marcey
|
||||
title: Technical Lead & Developer Advocate at Facebook
|
||||
url: http://twitter.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
twitter: JoelMarcey
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
twitter: sebastienlorber
|
||||
|
||||
yangshun:
|
||||
name: Yangshun Tay
|
||||
title: Front End Engineer at Facebook
|
||||
url: https://github.com/yangshun
|
||||
image_url: https://github.com/yangshun.png
|
||||
twitter: yangshunz
|
||||
|
||||
lex111:
|
||||
name: Alexey Pyltsyn
|
||||
title: Open-source enthusiast
|
||||
url: https://github.com/lex111
|
||||
image_url: https://github.com/lex111.png
|
|
@ -4,7 +4,7 @@ title: '📦 plugin-content-blog'
|
|||
slug: '/api/plugins/@docusaurus/plugin-content-blog'
|
||||
---
|
||||
|
||||
Provides the [Blog](blog.md) feature and is the default blog plugin for Docusaurus.
|
||||
Provides the [Blog](blog.mdx) feature and is the default blog plugin for Docusaurus.
|
||||
|
||||
## Installation {#installation}
|
||||
|
||||
|
@ -28,7 +28,7 @@ Accepted fields:
|
|||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | `string` | `'blog'` | Path to data on filesystem relative to site dir. |
|
||||
| `path` | `string` | `'blog'` | Path to the blog content directory on the filesystem, relative to site dir. |
|
||||
| `editUrl` | <code>string | EditUrlFunction</code> | `undefined` | Base URL to edit your site. The final URL is computed by `editUrl + relativeDocPath`. Using a function allows more nuanced control for each file. Omitting this variable entirely will disable edit links. |
|
||||
| `editLocalizedFiles` | `boolean` | `false` | The edit URL will target the localized file, instead of the original unlocalized file. Ignored when `editUrl` is a function. |
|
||||
| `blogTitle` | `string` | `'Blog'` | Blog page title for better SEO. |
|
||||
|
@ -49,6 +49,7 @@ Accepted fields:
|
|||
| `beforeDefaultRehypePlugins` | `any[]` | `[]` | Custom Rehype plugins passed to MDX before the default Docusaurus Rehype plugins. |
|
||||
| `truncateMarker` | `string` | `/<!--\s*(truncate)\s*-->/` | Truncate marker, can be a regex or string. |
|
||||
| `showReadingTime` | `boolean` | `true` | Show estimated reading time for the blog post. |
|
||||
| `authorsMapPath` | `string` | `'authors.yml'` | Path to the authors map file, relative to the blog content directory specified with `path`. Can also be a `json` file. |
|
||||
| `feedOptions` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. If undefined, no rss feed will be generated. |
|
||||
| `feedOptions.type` | <code>'rss' | 'atom' | 'all'</code> (or array of multiple options) | **Required** | Type of feed to be generated. |
|
||||
| `feedOptions.title` | `string` | `siteConfig.title` | Title of the feed. |
|
||||
|
@ -174,10 +175,11 @@ Accepted fields:
|
|||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `author` | `string` | `undefined` | The author name to be displayed. |
|
||||
| `author_url` | `string` | `undefined` | The URL that the author's name will be linked to. This could be a GitHub, Twitter, Facebook profile URL, etc. |
|
||||
| `author_image_url` | `string` | `undefined` | The URL to the author's thumbnail image. |
|
||||
| `author_title` | `string` | `undefined` | A description of the author. |
|
||||
| `authors` | `Authors` | `undefined` | List of blog post authors (or unique author). Read the [`authors` guide](../../blog.mdx#blog-post-authors) for more explanations. Prefer `authors` over the `author_*` FrontMatter fields, even for single author blog posts. |
|
||||
| `author` | `string` | `undefined` | ⚠️ Prefer using `authors`. The blog post author's name. |
|
||||
| `author_url` | `string` | `undefined` | ⚠️ Prefer using `authors`. The URL that the author's name will be linked to. This could be a GitHub, Twitter, Facebook profile URL, etc. |
|
||||
| `author_image_url` | `string` | `undefined` | ⚠️ Prefer using `authors`. The URL to the author's thumbnail image. |
|
||||
| `author_title` | `string` | `undefined` | ⚠️ Prefer using `authors`. A description of the author. |
|
||||
| `title` | `string` | Markdown title | The blog post title. |
|
||||
| `date` | `string` | File name or file creation time | The blog post creation date. If not specified, this can be extracted from the file or folder name, e.g, `2021-04-15-blog-post.mdx`, `2021-04-15-blog-post/index.mdx`, `2021/04/15/blog-post.mdx`. Otherwise, it is the Markdown file creation time. |
|
||||
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. |
|
||||
|
@ -192,6 +194,20 @@ Accepted fields:
|
|||
|
||||
```typescript
|
||||
type Tag = string | {label: string; permalink: string};
|
||||
|
||||
// An author key references an author from the global plugin authors.yml file
|
||||
type AuthorKey = string;
|
||||
|
||||
type Author = {
|
||||
key?: AuthorKey;
|
||||
name: string;
|
||||
title?: string;
|
||||
url?: string;
|
||||
image_url?: string;
|
||||
};
|
||||
|
||||
// The FrontMatter authors field allows various possible shapes
|
||||
type Authors = AuthorKey | Author | (AuthorKey | Author)[];
|
||||
```
|
||||
|
||||
Example:
|
||||
|
@ -199,10 +215,13 @@ Example:
|
|||
```yml
|
||||
---
|
||||
title: Welcome Docusaurus v2
|
||||
author: Joel Marcey
|
||||
author_title: Co-creator of Docusaurus 1
|
||||
author_url: https://github.com/JoelMarcey
|
||||
author_image_url: https://graph.facebook.com/611217057/picture/?height=200&width=200
|
||||
authors:
|
||||
- slorber
|
||||
- yangshun
|
||||
- name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
tags: [hello, docusaurus-v2]
|
||||
description: This is my first post on Docusaurus 2.
|
||||
image: https://i.imgur.com/mErPwqL.png
|
||||
|
@ -228,6 +247,7 @@ Read the [i18n introduction](../../i18n/i18n-introduction.md) first.
|
|||
website/i18n/<locale>/docusaurus-plugin-content-blog
|
||||
│
|
||||
│ # translations for website/blog
|
||||
├── authors.yml
|
||||
├── first-blog-post.md
|
||||
├── second-blog-post.md
|
||||
│
|
||||
|
|
|
@ -3,6 +3,9 @@ id: blog
|
|||
title: Blog
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
The blog feature enables you to deploy in no time a full-featured blog.
|
||||
|
||||
:::info
|
||||
|
@ -36,17 +39,23 @@ module.exports = {
|
|||
|
||||
To publish in the blog, create a Markdown file within the blog directory.
|
||||
|
||||
For example, create a file at `my-website/blog/2019-09-05-hello-docusaurus-v2.md`:
|
||||
For example, create a file at `website/blog/2019-09-05-hello-docusaurus-v2.md`:
|
||||
|
||||
```yml
|
||||
```yml title="website/blog/2019-09-05-hello-docusaurus-v2.md"
|
||||
---
|
||||
title: Welcome Docusaurus v2
|
||||
author: Joel Marcey
|
||||
author_title: Co-creator of Docusaurus 1
|
||||
author_url: https://github.com/JoelMarcey
|
||||
author_image_url: https://graph.facebook.com/611217057/picture/?height=200&width=200
|
||||
tags: [hello, docusaurus-v2]
|
||||
description: This is my first post on Docusaurus 2.
|
||||
slug: welcome-docusaurus-v2
|
||||
authors:
|
||||
- name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
- name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
tags: [hello, docusaurus-v2]
|
||||
image: https://i.imgur.com/mErPwqL.png
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
@ -158,9 +167,184 @@ module.exports = {
|
|||
};
|
||||
```
|
||||
|
||||
:::note
|
||||
## Blog post authors {#blog-post-authors}
|
||||
|
||||
Because the sidebar title is hard-coded in the configuration file, it is currently untranslatable.
|
||||
Use the `authors` FrontMatter field to declare blog post authors.
|
||||
|
||||
### Inline authors {#inline-authors}
|
||||
|
||||
Blog post authors can be declared directly inside the FrontMatter:
|
||||
|
||||
````mdx-code-block
|
||||
<Tabs
|
||||
defaultValue="single"
|
||||
values={[
|
||||
{label: 'Single author', value: 'single'},
|
||||
{label: 'Multiple authors', value: 'multiple'},
|
||||
]}>
|
||||
<TabItem value="single">
|
||||
|
||||
```yml title="my-blog-post.md"
|
||||
---
|
||||
authors:
|
||||
name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
---
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="multiple">
|
||||
|
||||
```yml title="my-blog-post.md"
|
||||
---
|
||||
authors:
|
||||
- name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
- name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
---
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
````
|
||||
|
||||
:::tip
|
||||
|
||||
This option works best to get started, or for casual, irregular authors.
|
||||
|
||||
:::
|
||||
|
||||
:::info
|
||||
|
||||
Prefer usage of the `authors` FrontMatter, but the legacy `author_*` FrontMatter remains supported:
|
||||
|
||||
```yml title="my-blog-post.md"
|
||||
---
|
||||
author: Joel Marcey
|
||||
author_title: Co-creator of Docusaurus 1
|
||||
author_url: https://github.com/JoelMarcey
|
||||
author_image_url: https://github.com/JoelMarcey.png
|
||||
---
|
||||
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Global authors {#global-authors}
|
||||
|
||||
For regular blog post authors, it can be tedious to maintain authors information inlined in each blog post.
|
||||
|
||||
It is possible declare those authors globally in a configuration file:
|
||||
|
||||
```yml title="website/blog/authors.yml"
|
||||
jmarcey:
|
||||
name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
|
||||
slorber:
|
||||
name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
Use the `authorsMapPath` plugin option to configure the path. JSON is also supported.
|
||||
|
||||
:::
|
||||
|
||||
In blog posts FrontMatter, you can reference the authors declared in the global configuration file:
|
||||
|
||||
````mdx-code-block
|
||||
<Tabs
|
||||
defaultValue="single"
|
||||
values={[
|
||||
{label: 'Single author', value: 'single'},
|
||||
{label: 'Multiple authors', value: 'multiple'},
|
||||
]}>
|
||||
<TabItem value="single">
|
||||
|
||||
```yml title="my-blog-post.md"
|
||||
---
|
||||
authors: jmarcey
|
||||
---
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="multiple">
|
||||
|
||||
```yml title="my-blog-post.md"
|
||||
---
|
||||
authors: [jmarcey, slorber]
|
||||
---
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
````
|
||||
|
||||
:::info
|
||||
|
||||
The `authors` system is very flexible and can suit more advanced use-case:
|
||||
|
||||
<details>
|
||||
<summary>Mix inline authors and global authors</summary>
|
||||
|
||||
You can use global authors most of the time, and still use inline authors:
|
||||
|
||||
```yml title="my-blog-post.md"
|
||||
---
|
||||
authors:
|
||||
- jmarcey
|
||||
- slorber
|
||||
- name: Inline Author name
|
||||
title: Inline Author Title
|
||||
url: https://github.com/inlineAuthor
|
||||
image_url: https://github.com/inlineAuthor
|
||||
---
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Local override of global authors</summary>
|
||||
|
||||
You can customize the global author's data on per-blog-post basis
|
||||
|
||||
```yml title="my-blog-post.md"
|
||||
---
|
||||
authors:
|
||||
- key: jmarcey
|
||||
title: Joel Marcey's new title
|
||||
- key: slorber
|
||||
name: Sébastien Lorber's new name
|
||||
---
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Localize the author's configuration file</summary>
|
||||
|
||||
The configuration file can be localized, just create a localized copy of it at:
|
||||
|
||||
```bash
|
||||
website/i18n/<locale>/docusaurus-plugin-content-blog/authors.yml
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
:::
|
||||
|
|
@ -81,6 +81,6 @@ You should delete the existing homepage at `./src/pages/index.js`, or else there
|
|||
|
||||
:::tip
|
||||
|
||||
There's also a "blog-only mode" for those who only want to use the blog feature of Docusaurus 2. You can use the same method detailed above. Follow the setup instructions on [Blog-only mode](../../blog.md#blog-only-mode).
|
||||
There's also a "blog-only mode" for those who only want to use the blog feature of Docusaurus 2. You can use the same method detailed above. Follow the setup instructions on [Blog-only mode](../../blog.mdx#blog-only-mode).
|
||||
|
||||
:::
|
||||
|
|
|
@ -90,7 +90,7 @@ my-website
|
|||
|
||||
### Project structure rundown {#project-structure-rundown}
|
||||
|
||||
- `/blog/` - Contains the blog Markdown files. You can delete the directory if you do not want/need a blog. More details can be found in the [blog guide](blog.md)
|
||||
- `/blog/` - Contains the blog Markdown files. You can delete the directory if you do not want/need a blog. More details can be found in the [blog guide](blog.mdx)
|
||||
- `/docs/` - Contains the Markdown files for the docs. Customize the order of the docs sidebar in `sidebars.js`. More details can be found in the [docs guide](./guides/docs/docs-markdown-features.mdx)
|
||||
- `/src/` - Non-documentation files like pages or custom React components. You don't have to strictly put your non-documentation files in here but putting them under a centralized directory makes it easier to specify in case you need to do some sort of linting/processing
|
||||
- `/src/pages` - Any files within this directory will be converted into a website page. More details can be found in the [pages guide](guides/creating-pages.md)
|
||||
|
|
|
@ -576,7 +576,7 @@ Refer to the [multi-language support code blocks](../guides/markdown-features/ma
|
|||
|
||||
The Docusaurus front matter fields for the blog have been changed from camelCase to snake_case to be consistent with the docs.
|
||||
|
||||
The fields `authorFBID` and `authorTwitter` have been deprecated. They are only used for generating the profile image of the author which can be done via the `author_image_url` field.
|
||||
The fields `authorFBID` and `authorTwitter` have been deprecated. They are only used for generating the profile image of the author which can be done via the `authors` field.
|
||||
|
||||
## Deployment {#deployment}
|
||||
|
||||
|
|
|
@ -270,7 +270,7 @@ const TwitterSvg =
|
|||
}
|
||||
return `https://github.com/facebook/docusaurus/edit/main/website/${blogDirPath}/${blogPath}`;
|
||||
},
|
||||
postsPerPage: 3,
|
||||
postsPerPage: 5,
|
||||
feedOptions: {
|
||||
type: 'all',
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue