Add reading time in Astro

Learn how to add a reading time to your Astro website (with code examples of course).
Published 2023-10-30 4 min read
Add reading time in Astro

Are you aiming to enhance your Astro blog’s user experience? I certainly was. One effective upgrade is adding an estimated reading time. Below is a step-by-step guide on integrating this feature into your Astro 3 blog with Static Site Generation (SSG) and markdown, inspired by what I implemented on this page.

Even if you’re using a different setup, the approach can be quite similar.

Prerequisites

Before getting started, ensure you have:

  • The Astro app setup, up and running
  • Some content or articles on your blog to test out the reading time function.

How to add a reading time to the Astro app?

What’s our strategy? We’ll count all words in the article and divide it by a sensible number of words per minute. For most adults it’s around 200 words per minute (238 to be exact). Of course, we have to connect it somehow to our template in the end.

Step 1. Create a new file

We need a new function. While you could declare it directly in the template file, that’s not ideal. If we would like to reuse this function it’s better to put it separate file. In my project, I created the following file src/utils/readingTime.js.

Step 2. Add code that counts the reading time

Let’s start with a full snippet and then we’ll analyze it a bit more.

const WORDS_PER_MINUTE = 200;

export default function readingTime(markdown) {
  if (!markdown) return;

  const numberOfWords = [...markdown.matchAll(/\w+/g)].length;
  const minutes = numberOfWords / WORDS_PER_MINUTE;
  return Math.ceil(minutes);
}

I created a function called readingTime and exported it as a default since it’s the only export in the entire file.

Then we’ll return undefined if there is no markdown, to prevent errors down the line. Next, the most important part.

We need to extract all words from the passed markdown. That’s why I created a regex that will match a word in the text. It’s not perfect, as it would count it's as two words or count some of the markdown stuff twice. To be honest it doesn’t matter that much, as it’s just an estimate. We could go with full-blown markdown parsing, but I don’t think it’s necessary or important.

Next this regex is passed to matchAll function, that will return all matches of the regex. Unfortunatelly matchAll returns an iterator which is not the best in our case.

Let’s convert it to an array with a spread operator and then query it for length.

Then let’s calculate minutes, we don’t have to cast to float, because all numbers in JS are floats. In order to display a nice number we need to round the result up.

Step 3. Import and use it

Now the function is ready and we can import it:

import readingTime from "../../utils/readingTime";

Let’s assume we already fetched our post, so we can use it like this:

<span>{`${readingTime(entry.body)} min read`}</span>

And that’s it for our case.

How about reading time if we have a plaintext?

That case is easier, as it’s perfectly reasonable to modify our function like this:

export default function readingTime(plainText) {
  if (!plainText) return;

  const numberOfWords = plainText.split(/\s/).length;
  const minutes = numberOfWords / WORDS_PER_MINUTE;
  return Math.ceil(minutes);
}

How about reading time if we have HTML?

This case is a bit trickier, as first we have to eliminate HTML tags.

export default function readingTime(html) {
  if (!html) return;

  const cleaned = html.replace(/(<([^>]+)>)/g, " ");
  const numberOfWords = plainText.split(/\s+/).length;
  const minutes = numberOfWords / WORDS_PER_MINUTE;
  return Math.ceil(minutes);
}

When it comes to replace function if we replace with empty string probably some words will be glued together. On the other hand, replacing it with whitespaces will result in multiple consecutive whitespaces. That’s why we modified split regex.

That’s all for today!

#astro