From 9ad6de2b85f554ca5fbc71d2b1140c77aad34f03 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 21 Oct 2021 21:26:10 +0800 Subject: [PATCH] feat(content-blog): new readingTime plugin option (#5702) --- .../src/__tests__/feed.test.ts | 4 + .../src/blogUtils.ts | 13 +- .../src/pluginOptionSchema.ts | 2 + .../src/types.ts | 20 +++ .../2021-10-07-blog-post-mdx-feed-tests.mdx | 1 + website/_dogfooding/dogfooding.config.js | 4 + .../docs/api/plugins/plugin-content-blog.md | 18 +++ website/docs/blog.mdx | 118 ++++++++++++++++++ 8 files changed, 179 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts index d2261547a7..2c2d0337a4 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -77,6 +77,8 @@ describe('blogFeed', () => { type: [feedType], copyright: 'Copyright', }, + readingTime: ({content, defaultReadingTime}) => + defaultReadingTime({content}), } as PluginOptions, ); @@ -111,6 +113,8 @@ describe('blogFeed', () => { type: [feedType], copyright: 'Copyright', }, + readingTime: ({content, defaultReadingTime}) => + defaultReadingTime({content}), } as PluginOptions, ); const feedContent = diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 011eb4fa6a..e50e4cadfc 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -16,6 +16,7 @@ import { BlogContentPaths, BlogMarkdownLoaderOptions, BlogTags, + ReadingTimeFunction, } from './types'; import { parseMarkdownFile, @@ -111,6 +112,10 @@ async function parseBlogPostMarkdownFile(blogSourceAbsolute: string) { }; } +const defaultReadingTime: ReadingTimeFunction = ({content, options}) => { + return readingTime(content, options).minutes; +}; + async function processBlogSourceFile( blogSourceRelative: string, contentPaths: BlogContentPaths, @@ -227,7 +232,13 @@ async function processBlogSourceFile( date, formattedDate, tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags), - readingTime: showReadingTime ? readingTime(content).minutes : undefined, + readingTime: showReadingTime + ? options.readingTime({ + content, + frontMatter, + defaultReadingTime, + }) + : undefined, truncated: truncateMarker?.test(content) || false, authors, }, diff --git a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts index 0d0e4cb755..00ede624e0 100644 --- a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts +++ b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts @@ -41,6 +41,7 @@ export const DEFAULT_OPTIONS: PluginOptions = { path: 'blog', editLocalizedFiles: false, authorsMapPath: 'authors.yml', + readingTime: ({content, defaultReadingTime}) => defaultReadingTime({content}), }; export const PluginOptionSchema = Joi.object({ @@ -113,4 +114,5 @@ export const PluginOptionSchema = Joi.object({ language: Joi.string(), }).default(DEFAULT_OPTIONS.feedOptions), authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath), + readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime), }); diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index 3d40724e5b..e8990d5437 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -12,6 +12,7 @@ import type { ContentPaths, } from '@docusaurus/utils/lib/markdownLinks'; import {Overwrite} from 'utility-types'; +import {BlogPostFrontMatter} from './blogFrontMatter'; export type BlogContentPaths = ContentPaths; @@ -46,6 +47,24 @@ export type EditUrlFunction = (editUrlParams: { locale: string; }) => 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; + options?: ReadingTimeOptions; +}) => number; + +export type ReadingTimeFunctionOption = ( + params: Required[0], 'options'>> & { + defaultReadingTime: ReadingTimeFunction; + }, +) => number | undefined; + export type PluginOptions = RemarkAndRehypePluginOptions & { id?: string; path: string; @@ -76,6 +95,7 @@ export type PluginOptions = RemarkAndRehypePluginOptions & { editLocalizedFiles?: boolean; admonitions: Record; authorsMapPath: string; + readingTime: ReadingTimeFunctionOption; }; // Options, as provided in the user config (before normalization) diff --git a/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx b/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx index 72db346e72..005d1b52e8 100644 --- a/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx +++ b/website/_dogfooding/_blog tests/2021-10-07-blog-post-mdx-feed-tests.mdx @@ -3,6 +3,7 @@ title: Blog post MDX Feed tests authors: - slorber 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 diff --git a/website/_dogfooding/dogfooding.config.js b/website/_dogfooding/dogfooding.config.js index ab35080206..be99600272 100644 --- a/website/_dogfooding/dogfooding.config.js +++ b/website/_dogfooding/dogfooding.config.js @@ -31,6 +31,10 @@ const dogfoodingPluginInstances = [ title: 'Docusaurus Tests Blog', copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, }, + readingTime: ({content, frontMatter, defaultReadingTime}) => + frontMatter.hide_reading_time + ? undefined + : defaultReadingTime({content, options: {wordsPerMinute: 5}}), }), ], diff --git a/website/docs/api/plugins/plugin-content-blog.md b/website/docs/api/plugins/plugin-content-blog.md index af7f8e0c9f..f769ab10b3 100644 --- a/website/docs/api/plugins/plugin-content-blog.md +++ b/website/docs/api/plugins/plugin-content-blog.md @@ -51,6 +51,7 @@ Accepted fields: | `beforeDefaultRehypePlugins` | `any[]` | `[]` | Custom Rehype plugins passed to MDX before the default Docusaurus Rehype plugins. | | `truncateMarker` | `string` | `//` | Truncate marker, can be a regex or string. | | `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. | | `feedOptions` | _See below_ | `{type: ['rss', 'atom']}` | Blog feed. If undefined, no rss feed will be generated. | | `feedOptions.type` | 'rss' \| 'atom' \| 'all' (or array of multiple options) | **Required** | Type of feed to be generated. | @@ -68,6 +69,23 @@ type EditUrlFunction = (params: { permalink: string; locale: string; }) => string | undefined; + +type ReadingTimeOptions = { + wordsPerMinute: number; + wordBound: (char: string) => boolean; +}; + +type ReadingTimeFunction = (params: { + content: string; + frontMatter?: BlogPostFrontMatter & Record; + options?: ReadingTimeOptions; +}) => number; + +type ReadingTimeFunctionOption = (params: { + content: string; + frontMatter: BlogPostFrontMatter & Record; + defaultReadingTime: ReadingTimeFunction; +}) => number | undefined; ``` ## Example configuration {#ex-config} diff --git a/website/docs/blog.mdx b/website/docs/blog.mdx index ecb0312384..171cc12c76 100644 --- a/website/docs/blog.mdx +++ b/website/docs/blog.mdx @@ -335,6 +335,124 @@ website/i18n//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 + + + +**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! +``` + + + + +**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 + }, + }, + ], + ], +}; +``` + + + + +**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), + }, + }, + ], + ], +}; +``` + + + +```` + +::: + ## 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`.