Adding reading time to a blog: a small feature that makes a difference

That little “3 min read” label takes only a few lines of logic. Here’s how I calculate it from Markdown in .NET and display it in Angular.

Reading-time illustration showing an article card with a 3 min read label and the flow from Markdown to word count to reading time.
A small reading-time label, calculated from Markdown on the server.

While working on a blog, I wanted readers to know roughly how much time an article would take. Nothing complicated. Just a small label beside the date:

3 min read

The calculation is simple. The interesting part is deciding what to count.

Start with the smallest useful formula

I use 220 words per minute as a configurable assumption—not a promise about how fast everyone reads.

public static int EstimateMinutes(int words)
{
    return Math.Max(1, (int)Math.Ceiling(words / 220d));
}

Why ceiling? Because 1.4 minutes should display as 2, not 1. Why a minimum of 1? Because “0 min read” looks odd on a short post.

Counted words Calculation Label
100 100 ÷ 220 = 0.45 1 min read
220 220 ÷ 220 = 1 1 min read
221 221 ÷ 220 = 1.005 2 min read
500 500 ÷ 220 = 2.27 3 min read

Count the writing, not the Markdown syntax

Counting the raw source also counts things readers never see, such as link destinations. Instead, I parse Markdown with Markdig and walk its text nodes.

This is a focused version of the calculation used on this blog. Add Markdig to your .NET project:

dotnet add package Markdig
using Markdig;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using System.Text.RegularExpressions;

public static class ReadingTime
{
    private static readonly MarkdownPipeline Pipeline =
        new MarkdownPipelineBuilder().UsePipeTables().Build();

    public static int Calculate(string markdown)
    {
        var document = Markdown.Parse(markdown, Pipeline);
        var words = 0;

        foreach (var node in document.Descendants())
        {
            if (node is LiteralInline literal)
            {
                words += Regex.Matches(
                    literal.Content.ToString(),
                    @"[\p{L}\p{N}]+(?:['’-][\p{L}\p{N}]+)*").Count;
            }
        }

        return Math.Max(1, (int)Math.Ceiling(words / 220d));
    }
}

Headings, paragraphs and list text contribute to the count. Formatting markers and link URLs do not. The pattern accepts Unicode letters and numbers and keeps words such as “don’t” together. It is a practical heuristic for this blog’s content, not a universal tokenizer for every language.

For example:

## A small improvement

Read the [setup guide](https://example.com/a/very/long/path).

The heading and link label count. The long URL does not. Image alternative text can also contribute because it is represented as literal text.

What about code examples?

In this implementation, fenced code blocks and inline code are excluded: they are different node types, not LiteralInline.

That keeps a large pasted sample from inflating the estimate. It also has a tradeoff: understanding code may take longer than reading the surrounding explanation.

I treat the label as prose reading time, not the time needed to complete the tutorial. A post with 500 counted words still shows “3 min read” even if it contains a long code block. A code-only post gets the minimum “1 min read”.

If your readers need a hands-on estimate, you could add a separate “About 15 minutes to try” note. That is a different measurement.

Calculate it once on the server

When an article is saved, the backend calculates the value. When an immutable revision is created, it calculates and stores that revision’s value too. Public pages receive the estimate belonging to the published revision.

That last detail matters: editing a draft should not change the reading time of the article people are currently reading.

The API returns a number:

{
  "title": "A small improvement",
  "readingTimeMinutes": 3
}

Angular only displays it:

<span>{{ article.readingTimeMinutes }} min read</span>

There is no second word-counting algorithm in the browser. Article cards and the article page use the same value, including server-rendered pages.

Check the boundaries

These are useful cases to keep in a test suite:

  • 220 plain words → 1 minute.
  • 221 plain words → 2 minutes.
  • A long fenced code block → no extra counted words.
  • A link with a long URL → count its label, not its destination.
  • A draft edit → leave the published revision’s estimate unchanged.

A reading-time label does not need to be perfectly precise to be helpful. It just needs a clear rule, a consistent result, and a modest place beside the article metadata.

Share this article

Facebook LinkedIn WhatsApp X

For Instagram or another app, copy the link or use More options when available.

Bujar Ademi

Written by

Bujar Ademi

Senior .NET Developer & Team Lead

← All articles