A guide to create a remark plugin to make the reading time data available when importing MDX files as ES modules.
Remark is a powerful markdown processor that can be used to create custom plugins to transform markdown content. When parsing markdown files with remark, the content is transformed into an abstract syntax tree (AST) that can be manipulated using plugins.
For a better user experience, it's common to display the estimated reading time of an article. In this guide, we'll create a remark plugin to extract the reading time data from an MDX file and make it available when importing the MDX file as an ES module.
Get started
Let's start by creating an MDX file:
# Hello, world!
This is an example MDX file.
Assuming we are using Vite as the bundler, with the official @mdx-js/rollup
plugin to transform MDX files, thus we can import the MDX file as an ES module. The Vite configuration should look like this:
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
{
// The `enforce: 'pre'` is required to make the MDX plugin work
enforce: 'pre',
...mdx({
// ...configurations
}),
},
],
});
If we import the MDX file as an ES module, the content will be an object with the default
property containing the compiled JSX. For example:
const mdx = await import('./example.mdx');
console.log(mdx);
Will yield:
{
// ...other properties if you have plugins to transform the MDX content
default: [Function: MDXContent],
}
Once we have the output like this, we can are ready to create the remark plugin.
Create the remark plugin
Let's check out what we need to do to achieve the goal:
- Extract MDX content to text for reading time calculation.
- Calculate the reading time.
- Attach the reading time data to the MDX content, make it available when importing the MDX file as an ES module.
Luckily, there are already libraries to help us with the reading time calculation and basic AST operations:
-
reading-time
to calculate the reading time. -
mdast-util-to-string
to convert the MDX AST to text. -
estree-util-value-to-estree
to convert the reading time data to an ESTree node.
If you are a TypeScript user, you may also need to install these packages for type definitions:
-
@types/mdast
for MDX root node type definitions. -
unified
for plugin type definitions.
As long as we have the packages installed, we can start creating the plugin:
import { type Root } from 'mdast';
import { toString } from 'mdast-util-to-string';
import getReadingTime from 'reading-time';
import { type Plugin } from 'unified';
// The first argument is the configuration, which is not needed in this case. You can update the
// type if you need to have a configuration.
export const remarkMdxReadingTime: Plugin<void[], Root> = function () {
return (tree) => {
const text = toString(tree);
const readingTime = getReadingTime(text);
// TODO: Attach the reading time data to the MDX content
};
};
As we can see, the plugin simply extracts the MDX content to text and calculates the reading time. Now we need to attach the reading time data to the MDX content, and it looks not that straightforward. But if we take a look at the other awesome libraries like remark-mdx-frontmatter, we can find a way to do it:
import { valueToEstree } from 'estree-util-value-to-estree';
import { type Root } from 'mdast';
import { toString } from 'mdast-util-to-string';
import getReadingTime from 'reading-time';
import { type Plugin } from 'unified';
export const remarkMdxReadingTime: Plugin<void[], Root> = function () {
return (tree) => {
const text = toString(tree);
const readingTime = getReadingTime(text);
tree.children.unshift({
type: 'mdxjsEsm',
value: '',
data: {
estree: {
type: 'Program',
sourceType: 'module',
body: [
{
type: 'ExportNamedDeclaration',
specifiers: [],
declaration: {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: 'readingTime' },
init: valueToEstree(readingTime, { preserveReferences: true }),
},
],
},
},
],
},
},
});
};
};
Note the type: 'mdxjsEsm'
in the code above. This is a node type that is used to serialize MDX ESM. The code above attaches the reading time
data using the name readingTime to the MDX content, which will yield the following output when importing the MDX file as an ES module:
{
default: [Function: MDXContent],
readingTime: { text: '1 min read', minutes: 0.1, time: 6000, words: 2 }, // The reading time data
}
If you need to change the name of the reading time data, you can update the name
property of the Identifier
node.
TypeScript support
To make the plugin even more developer-friendly, we can make one last touch by augmenting the MDX type definitions:
declare module '*.mdx' {
import { type ReadTimeResults } from 'reading-time';
export const readingTime: ReadTimeResults;
// ...other augmentations
}
Now, when importing the MDX file, TypeScript will recognize the readingTime
property:
import { readingTime } from './example.mdx';
console.log(readingTime); // { text: '1 min read', minutes: 0.1, time: 6000, words: 2 }
Conclusion
I hope this guide helps you have a better experience when working with MDX files. With this remark plugin, you can use the reading time data directly and even leverage ESM tree-shaking for better performance.
Top comments (0)