feat(content-blog): new readingTime plugin option (#5702)

This commit is contained in:
Joshua Chen 2021-10-21 21:26:10 +08:00 committed by GitHub
parent 92002b6bd3
commit 9ad6de2b85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 179 additions and 1 deletions

View file

@ -77,6 +77,8 @@ describe('blogFeed', () => {
type: [feedType], type: [feedType],
copyright: 'Copyright', copyright: 'Copyright',
}, },
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
} as PluginOptions, } as PluginOptions,
); );
@ -111,6 +113,8 @@ describe('blogFeed', () => {
type: [feedType], type: [feedType],
copyright: 'Copyright', copyright: 'Copyright',
}, },
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
} as PluginOptions, } as PluginOptions,
); );
const feedContent = const feedContent =

View file

@ -16,6 +16,7 @@ import {
BlogContentPaths, BlogContentPaths,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
BlogTags, BlogTags,
ReadingTimeFunction,
} from './types'; } from './types';
import { import {
parseMarkdownFile, parseMarkdownFile,
@ -111,6 +112,10 @@ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) {
}; };
} }
const defaultReadingTime: ReadingTimeFunction = ({content, options}) => {
return readingTime(content, options).minutes;
};
async function processBlogSourceFile( async function processBlogSourceFile(
blogSourceRelative: string, blogSourceRelative: string,
contentPaths: BlogContentPaths, contentPaths: BlogContentPaths,
@ -227,7 +232,13 @@ async function processBlogSourceFile(
date, date,
formattedDate, formattedDate,
tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags), tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
readingTime: showReadingTime ? readingTime(content).minutes : undefined, readingTime: showReadingTime
? options.readingTime({
content,
frontMatter,
defaultReadingTime,
})
: undefined,
truncated: truncateMarker?.test(content) || false, truncated: truncateMarker?.test(content) || false,
authors, authors,
}, },

View file

@ -41,6 +41,7 @@ export const DEFAULT_OPTIONS: PluginOptions = {
path: 'blog', path: 'blog',
editLocalizedFiles: false, editLocalizedFiles: false,
authorsMapPath: 'authors.yml', authorsMapPath: 'authors.yml',
readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}),
}; };
export const PluginOptionSchema = Joi.object<PluginOptions>({ export const PluginOptionSchema = Joi.object<PluginOptions>({
@ -113,4 +114,5 @@ export const PluginOptionSchema = Joi.object<PluginOptions>({
language: Joi.string(), language: Joi.string(),
}).default(DEFAULT_OPTIONS.feedOptions), }).default(DEFAULT_OPTIONS.feedOptions),
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath), authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),
}); });

View file

@ -12,6 +12,7 @@ import type {
ContentPaths, ContentPaths,
} from '@docusaurus/utils/lib/markdownLinks'; } from '@docusaurus/utils/lib/markdownLinks';
import {Overwrite} from 'utility-types'; import {Overwrite} from 'utility-types';
import {BlogPostFrontMatter} from './blogFrontMatter';
export type BlogContentPaths = ContentPaths; export type BlogContentPaths = ContentPaths;
@ -46,6 +47,24 @@ export type EditUrlFunction = (editUrlParams: {
locale: string; locale: string;
}) => string | undefined; }) => string | undefined;
// Duplicate from ngryman/reading-time to keep stability of API
type ReadingTimeOptions = {
wordsPerMinute?: number;
wordBound?: (char: string) => boolean;
};
export type ReadingTimeFunction = (params: {
content: string;
frontMatter?: BlogPostFrontMatter & Record<string, unknown>;
options?: ReadingTimeOptions;
}) => number;
export type ReadingTimeFunctionOption = (
params: Required<Omit<Parameters<ReadingTimeFunction>[0], 'options'>> & {
defaultReadingTime: ReadingTimeFunction;
},
) => number | undefined;
export type PluginOptions = RemarkAndRehypePluginOptions & { export type PluginOptions = RemarkAndRehypePluginOptions & {
id?: string; id?: string;
path: string; path: string;
@ -76,6 +95,7 @@ export type PluginOptions = RemarkAndRehypePluginOptions & {
editLocalizedFiles?: boolean; editLocalizedFiles?: boolean;
admonitions: Record<string, unknown>; admonitions: Record<string, unknown>;
authorsMapPath: string; authorsMapPath: string;
readingTime: ReadingTimeFunctionOption;
}; };
// Options, as provided in the user config (before normalization) // Options, as provided in the user config (before normalization)

View file

@ -3,6 +3,7 @@ title: Blog post MDX Feed tests
authors: authors:
- slorber - slorber
tags: [blog, docusaurus, long-long, long-long-long, long-long-long-long] tags: [blog, docusaurus, long-long, long-long-long, long-long-long-long]
hide_reading_time: true
--- ---
Some MDX tests, mostly to test how the RSS feed render those Some MDX tests, mostly to test how the RSS feed render those

View file

@ -31,6 +31,10 @@ const dogfoodingPluginInstances = [
title: 'Docusaurus Tests Blog', title: 'Docusaurus Tests Blog',
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
}, },
readingTime: ({content, frontMatter, defaultReadingTime}) =>
frontMatter.hide_reading_time
? undefined
: defaultReadingTime({content, options: {wordsPerMinute: 5}}),
}), }),
], ],

View file

@ -51,6 +51,7 @@ Accepted fields:
| `beforeDefaultRehypePlugins` | `any[]` | `[]` | Custom Rehype plugins passed to MDX before the default Docusaurus Rehype plugins. | | `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. | | `truncateMarker` | `string` | `/<!--\s*(truncate)\s*-->/` | Truncate marker, can be a regex or string. |
| `showReadingTime` | `boolean` | `true` | Show estimated reading time for the blog post. | | `showReadingTime` | `boolean` | `true` | Show estimated reading time for the blog post. |
| `readingTime` | `ReadingTimeFunctionOption` | The default reading time | A callback to customize the reading time number displayed. |
| `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. | | `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` | _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.type` | <code>'rss' \| 'atom' \| 'all'</code> (or array of multiple options) | **Required** | Type of feed to be generated. |
@ -68,6 +69,23 @@ type EditUrlFunction = (params: {
permalink: string; permalink: string;
locale: string; locale: string;
}) => string | undefined; }) => string | undefined;
type ReadingTimeOptions = {
wordsPerMinute: number;
wordBound: (char: string) => boolean;
};
type ReadingTimeFunction = (params: {
content: string;
frontMatter?: BlogPostFrontMatter & Record<string, unknown>;
options?: ReadingTimeOptions;
}) => number;
type ReadingTimeFunctionOption = (params: {
content: string;
frontMatter: BlogPostFrontMatter & Record<string, unknown>;
defaultReadingTime: ReadingTimeFunction;
}) => number | undefined;
``` ```
## Example configuration {#ex-config} ## Example configuration {#ex-config}

View file

@ -335,6 +335,124 @@ website/i18n/<locale>/docusaurus-plugin-content-blog/authors.yml
::: :::
## Reading time {#reading-time}
Docusaurus generates a reading time estimation for each blog post based on word count. We provide an option to customize this.
```js title="docusaurus.config.js"
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
blog: {
// highlight-start
showReadingTime: true, // When set to false, the "x min read" won't be shown
readingTime: ({content, frontMatter, defaultReadingTime}) =>
defaultReadingTime({content, options: {wordsPerMinute: 300}}),
// highlight-end
},
},
],
],
};
```
The `readingTime` callback receives three parameters: the blog content text as a string, front matter as a record of string keys and their values, and the default reading time function. It returns a number (reading time in minutes) or `undefined` (disable reading time for this page).
The default reading time is able to accept additional options: `wordsPerMinute` as a number (default: 300), and `wordBound` as a function from string to boolean. If the string passed to `wordBound` should be a word bound (spaces, tabs, and line breaks by default), the function should return `true`.
:::tip
Use the callback for all your customization needs:
````mdx-code-block
<Tabs>
<TabItem value="disable-per-post" label="Per-post disabling">
**Disable reading time on one page:**
```js title="docusaurus.config.js"
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
blog: {
showReadingTime: true,
// highlight-start
readingTime: ({content, frontMatter, defaultReadingTime}) =>
frontMatter.hide_reading_time ? undefined : defaultReadingTime({content}),
// highlight-end
},
},
],
],
};
```
Usage:
```yml "my-blog-post.md"
---
hide_reading_time: true
---
This page will no longer display the reading time stats!
```
</TabItem>
<TabItem value="passing-options" label="Passing options">
**Pass options to the default reading time function:**
```js title="docusaurus.config.js"
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
blog: {
// highlight-start
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content, options: {wordsPerMinute: 100}}),
// highlight-end
},
},
],
],
};
```
</TabItem>
<TabItem value="using-custom-algo" label="Using custom algorithms">
**Use a custom implementation of reading time:**
```js title="docusaurus.config.js"
const myReadingTime = require('./myReadingTime');
module.exports = {
presets: [
[
'@docusaurus/preset-classic',
{
blog: {
// highlight-next-line
readingTime: ({content}) => myReadingTime(content),
},
},
],
],
};
```
</TabItem>
</Tabs>
````
:::
## Feed {#feed} ## Feed {#feed}
You can generate RSS/Atom feed by passing feedOptions. By default, RSS and Atom feeds are generated. To disable feed generation, set `feedOptions.type` to `null`. You can generate RSS/Atom feed by passing feedOptions. By default, RSS and Atom feeds are generated. To disable feed generation, set `feedOptions.type` to `null`.