DEV Community

Palomino for Logto

Posted on • Originally published at blog.logto.io

Create a remark plugin to extract MDX reading time

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.
Enter fullscreen mode Exit fullscreen mode

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
      }),
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Will yield:

{
  // ...other properties if you have plugins to transform the MDX content
  default: [Function: MDXContent],

}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Extract MDX content to text for reading time calculation.
  2. Calculate the reading time.
  3. 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
  };
};
Enter fullscreen mode Exit fullscreen mode

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 }),
                  },
                ],
              },
            },
          ],
        },
      },
    });
  };
};
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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.

Try Logto Cloud for free

Top comments (0)