Word counts and reading time in Jekyll
Displaying a post’s word count is rather common when creating a blog, but usually those techniques rely on JavaScript to work. The script reads the post’s text, counts the words and displays the result accordingly. That was the way I did things on this blog first as well, but then I set out to find a better way.
If you just want to get to the snippets without the explanations, they're available in the paragraph Summing it up.
Showing the word count
Luckily Jekyll provides a handy liquid filter called number_of_words. So displaying the actual word count is as simple as that:
{{ page.content | number_of_words }}
While this works just nicely it’s not very solid. You might want to hide word counts on shorter posts, for example as they’re of little value in such posts. This is a little more complex as you can not directly use Liquid filters in a conditional block.
Variables in Liquid
In Liquid there are two ways to create variables. You can {% assign %} a variable and you can {% capture %} a variable. The difference might not be obvious, but it’s simple once you get it.
Assigning a value to a variable means that you take any kind of data (e.g. a string, a number, a boolean) and Liquid knows that you want to access that exact data when you refer to this variable. An assigned variable is fixed, that means you can not use the value returned from other Liquid tags.
{% assign awesome = true %}
{% if awesome %}
<p>Hello, world!</p>
{% endif %}
But what if you want to store a Liquid tags’s return value in a variable? That’s exactly what the {% capture %} block is for. Unlike assigned variables, captured variables can only hold strings — which will cause us some trouble later on. This is simply because Liquid tags return strings by default.
{% capture value %}
{{ page.title | upcase }} from {{ page.date | date: "%b %d, %y" }}
{% endcapture %}
As you can see in the above example, you can capture any number of strings into a variable, be it strings returned from a Liquid tag or fixed strings.
Making the word count conditional
Now that you know about {% assign %} and {% capture %} we can move on to store our word count in a variable. The question remains, do we assign the variable or do we capture it?
It should be clear by now that we’ll have to capture the value as it’s returned from a Liquid tag. That gives us something like this:
{% capture words %}
{{ page.content | number_of_words }}
{% endcapture %}
Let’s say we consider posts shorter than 250 words not worth displaying a word count for. A good example of this would be a ‘link list’-style post that consists mostly of a quote from the original article and a comment spanning a sentence or two. Ideally, this would be handled using a simple conditional block.
{% if words > 250 %}
{{ words }}
{% endif %}
However, this won’t work as intended, since Jekyll will throw an error saying you’ve attempted to compare a string (the word count) with a number (250), which is correct but not supported. There is, however, a simple workaround.
{% capture words %}
{{ page.content | number_of_words | minus: 250 }}
{% endcapture %}
{% unless words contains "-" %}
{{ words | plus: 250 }}
{% endunless %}
You can use Liquid filters to subtract your minimum number from the word count to see if it falls below 0. If it does, it will contain a ’-’ at the beginning, which means the post is too short and won’t have its word count displayed. If our variable doesn’t contain a ’-’, we can simply add our minimum number back to the word count and display it.
Customising the output
Now that we finally have our word number along with the conditional to hide it from short posts we can move on to make the output a bit nicer. You do this using Liquid filters like append or prepend. For a complete list of available filters you can check Shopify’s Liquid for Designers guide.
{{ words | plus: 250 | append: " words" }}
The above snippet results in something like ‘There are 250 words in this post’. There are many filters available, offering a wide range of possibilities.
Calculating the reading time
You might have noticed that I display an estimated reading time on this blog instead of just a word count. Personally, I just think this is a more useful guideline. Doing this is as easy as putting the divided_by filter into our final word count construct. The number to divide by is arbitrary, but 180 is the average number of words a person reads per minute.
{{ words | plus: 250 | divided_by: 180 | append: " minutes to read" }}
Summing it up
Instead of making you pick up all the pieces scattered all over this post, here are the two snippets to display the word count and the reading time.
{% capture words %}
{{ page.content | number_of_words | minus: 250 }}
{% endcapture %}
{% unless words contains "-" %}
{{ words | plus: 250 | append: " words" }}
{% endunless %}
{% capture words %}
{{ page.content | number_of_words | minus: 250 }}
{% endcapture %}
{% unless words contains "-" %}
{{ words | plus: 250 | divided_by: 180 | append: " minute read" }}
{% endunless %}