I added reading time to this site yesterday: the little “3 min read” line under each post title. There’s a whole npm ecosystem for this, but instead I wrote a thirty-line remark plugin.

Why not just npm i reading-time?

The package works fine, but every dependency bets on the same core things:

  • that the author keeps maintaining it
  • that its transitive deps don’t drift
  • that its type definitions stay in sync with your toolchain
  • that you remember why it’s in the lockfile six months from now

For a feature whose entire spec is “divide by 220 and round,” that bet costs more than it pays.

The whole thing

const WORDS_PER_MINUTE = 220;

function nodeToText(node) {
  if (!node || typeof node !== 'object') return '';
  if (typeof node.value === 'string') return node.value;
  if (Array.isArray(node.children)) {
    return node.children.map(nodeToText).join(' ');
  }
  return '';
}

export function remarkReadingTime() {
  return (tree, file) => {
    const text = nodeToText(tree);
    const words = text.trim().split(/\s+/).filter(Boolean).length;
    const minutes = Math.max(1, Math.round(words / WORDS_PER_MINUTE));
    file.data.astro.frontmatter.minutesRead = minutes;
  };
}

Wire it into astro.config.mjs under markdown.remarkPlugins, then read remarkPluginFrontmatter.minutesRead from await render(post) in your layout. Done.

The rule

If the library is shorter than its README, write it yourself.