Refactoring Nunjucks Templates

(or "Components Before It Was Cool")

Wednesday 21 June 2023 • 1,147 words

This is the third article in the "I Made A Website" series. If you haven't read parts one and two, don't forget to check them out to get the full background.

I added word counts to my articles. Primarily so that if you are about to read something of mine, you know what you're in for. Or more accurately, how long the ride's going to be. This gave me the opportunity to refactor my site's Nunjucks templates a bit, and made me think about components on the Web. This post may look code-heavy, but I assure you there is nothing complex happening here.

Word Counts

I'll get the word counts bit out of the way first. I'm using a solution provided by Bryce Wray (thank you!), but with the reading time removed (I don't think it can be accurate for the various types of people reading my content). I'm doing this directly in Nunjucks, and it's 3 lines of templating:

{% set regExpCode = r/<pre class=(.|\n)*?<\/pre>/gm %}
{% set fixedContent = content | replace(regExpCode, "") | striptags %}
{% set wordCount = fixedContent | wordcount %}

As Bryce says in his post, set regExpCode builds a regular expression that searches for the HTML stuff that Eleventy's syntax highlighting plugin adds to code blocks. That regex matches anything inside <pre></pre> tags (that have any class), an example being the code block you saw above this paragraph. Open your developer tools (F12 in Chrome/Firefox) and poke around. Stay curious!

We don't want to count stuff inside code blocks as "words" for the purposes of an article's word count. set fixedContent uses the regex we made to get rid of code blocks in the post's content, then striptags gets rid of all the tags and chomps superfluous whitespace.

Still with me? Next part's really easy. set wordCount runs the clean string we now have through Nunjucks' wordcount. Which is almost perfect. We then need a one-liner in eleventy.config.js:

eleventyConfig.addFilter("numCommas", (value) => value.toLocaleString());

Which just puts this word count number we have, a very literal integer, through JavaScript's Number.prototype.toLocaleString() to get something a little more readable. For example; word counts of 1000 will appear as '1,000'. Bryce chose to do a standard function, but I made it an arrow function to make it a one-liner.

And we use it in the layout like so:

<h3>{% niceDate page.date %} &bullet; {{ wordCount | numCommas }} words</h3>

You can also see my Eleventy shortcode niceDate here, as a bonus. You could, if you wanted, instead do all of the word counting through a shortcode/filter in a similar way, but I think Bryce's solution is really clean, especially when we abstract it away into a Nunjucks macro.

Components?

At the same time as adding the word count, I did some refactoring of my Nunjucks templates so that I had some reusable parts, or components, if you will.

Back when I was set on using React for my website, I had written a fair few components. For the uninitiated, components are just reusable pieces of code. You could just as well call them functions (they are) but on the Web they are slightly different as they compose not only functionality but also markup. Thus, web components = wrapping parts of markup and functionality into a function that you can pass parameters into, keeping your code DRY (Do {not} Repeat Yourself). In the world of lightweight SSGs, (we're not talking about Gatsby here because that's literally just React With Addons), components are still really useful! Nobody wants to copy and paste the same thing over and over (with all the problems that comes with) and it's effective for code splitting at a smaller level than just templates/layouts.

Let's do an example; there are multiple places on my site where I show blog post information (that is, the title, subtitle, post date, word count) and they're presenting the same information, using the same code. So let's encapsulate that into a function we can pass a post object into. This makes use of Nunjucks' macro. According to the documentation, "[it] allows you to define reusable chunks of content." Perfect.

Here's my entire macro:

{% macro postInfo(post) %}

{% set regExpCode = r/<pre class=(.|\n)*?<\/pre>/gm %}
{% set fixedContent = post.content | replace(regExpCode, "") | striptags %}
{% set wordCount = fixedContent | wordcount %}

<h2><a href="{{ post.url }}">{{ post.data.title }}</a></h2>
<p>{{ post.data.subtitle }}</p>
<small>{% niceDate post.date %} &bullet; {{ wordCount | numCommas }} words</small>

{% endmacro %}

Hey look! There's that word count templating stuff! And some HTML to render it out nicely. Then it becomes a case of using that function wherever I need post info. Maybe that's on the home page to display the latest post:

{%- from "components/postInfo/macro.njk" import postInfo -%}
  {% for post in collections.post | reverse | first %}
  {{ postInfo(post) }}
{% endfor %}

Or on the blog page, to make a list of all posts:

{%- from "components/postInfo/macro.njk" import postInfo -%}
  {% for post in collections.post | reverse %}
  {{ postInfo(post) }}
{% endfor %}

And yes, the only difference between those two is the addition of running the posts collection through Nunjucks' first filter to get a single item for the home page. That's something I'll probably come back to with a collection filter, but it works fine for now.

If you've used React (or anything similar) you'll recognise the paradigm:

This isn't specific to Nunjucks, given that it's effectively a port of Jinja2 (from 2008). Component-based design has been around for a long, long time. To make the point, we didn't need full-fat JavaScript libraries to do this. We can enjoy modern takes on development but do it lean! At the time of writing, if you're reading my website you haven't downloaded any client side JavaScript. With the cache disabled, if you hit my last post you'll transfer 1.03MB. 984kB of which is the jpeg in the footer.

Eleventy does have first-class support for 'WebC', a new framework-independent web-component language. You'll note that this is another project of Zach Leatherman (the fine fellow behind Eleventy). I haven't gone down this route (because I haven't actually explored it yet), but it looks quite powerful and a good replacement for some of the templating languages that are beginning to show their age. Zach's said before that Nunjucks isn't that well maintained. To be frank, I'd agree; their documentation is unfortunately lacking more concrete examples and some parts of it effectively say "dunno, go look at jinja2's docs".

I will most likely continue to use Nunjucks, though, because I've started (so I'll finish) and so far I haven't hit any edge cases that make me want to rip everything up and start afresh. That isn't to say that I'm going to ignore the trend away from these dedicated templating languages: I'll keep an eye on WebC - but so many in frontend do the JavaScript equivalent of distro hopping every few months as the latest framework hits GitHub. Just because something new turns up doesn't mean the thing you were just using has suddenly become useless!