Possum websites, done possumly

Part 4: Filters

9 minute read time / 1110 words

Filters are a powerful tool, and the best part is you can expand on them however you want if you can write a bit of JavaScript!

Recap

In part three we covered setting up copying images and fonts through to the output directory, configuring Eleventy folders to your liking, and getting the basic global navigation working.

Filters

As briefly mentioned in a previous part, Nunjucks has a bunch of built-in filters. With your handy .eleventy.js config file, we can add our own custom filters.

This is so handy! I've needed to expand functionality many times and adding a simple filter is sometimes all you need. This site uses a few custom filters which help me with formatting article dates nicely for both human readable formats and computer readable formats, filters to only pull in articles if published, featured or 'pinned' so that I can articles in specific locations easily.

I've also got a handy filter called contains that I use to help determine if the current page should be set active in my navigation - more on this shortly.

This is how our Eleventy configuration file is looking:

module.exports = function(eleventyConfig) {
    eleventyConfig.addPassthroughCopy({ 'public/fonts': 'fonts' });
    eleventyConfig.addPassthroughCopy({ 'public/img': 'img' });

    return {
        passthroughFileCopy: true
    };
}

Custom filters

To add a custom filter, we can insert our code after the addPassthroughCopy and before the return statement.

niceDate and niceDateTime

For date and datetime jiggery-pokery, we need to specify before the module.exports function a couple of extra bits.

const englishDateOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: 'UTC'
};

const englishFormat = new Intl.DateTimeFormat('en-GB', englishDateOptions);

Adjust to accommodate your local date and time requirements, and then we're good to get the filter added.

eleventyConfig.addFilter('niceDate', function(dateString) {
    return englishFormat.format(dateString);
});

eleventyConfig.addFilter('niceDateTime', function(dateString) {
    const date = new Date(dateString);

    return date.toISOString();
});

If we take our date value from a page's Frontmatter, and run it through the niceDate filter, we get a friendlier format out which looks just like the published date you see on this site for post listings and under the main page title on a full article page: Tuesday, 27 February 2024.

The niceDateTime filter takes the same date value and formats it nicely for a <time datetime=""&;gt> value.

onlyIfPublished, onlyIfFeatured, onlyIfPinned

These three filters are almost indentical, other than which Frontmatter value they're looking for. It's possible these could all use the same filter and just pass the key for the value we're searching for to make it a much more flexible filter, but for how I'm using them at the moment, I've opted to keep them a separate concerns.

eleventyConfig.addFilter('onlyIfPublished', function(posts) {
    let result = posts.filter(p => {
        return p.data.ispublished === true;
    });

    return result;
});

eleventyConfig.addFilter('onlyIfFeatured', function(posts) {
    let result = posts.filter(p => {
        return p.data.isfeatured === true;
    });

    return result;
});

eleventyConfig.addFilter('onlyIfPinned', function(posts) {
    let result = posts.filter(p => {
        return p.data.ispinned === true;
    });

    return result;
});

If we wanted to make this a generic filter that you could use to check for the value of anything in Frontmatter, we could do something like this (untested!):

eleventyConfig.addFilter('onlyIf', function(posts, key, value) {
    let result = posts.filter(p => {
        return p.datap[key] === value;
    });

    return result;
});

contains

This filter helps to check if the current page's URL contains the URL of the entry we're currently outputting a navigation link for. This can be used in probably more places than navigation, but that's its main purpose.

eleventyConfig.addFilter('contains', (value, needle = '', haystack = []) => {
    const isValueFound = haystack.some(hay => needle.includes(hay)) ? value : false;

    return isValueFound;
});

This is used in any place where a nav link is output on this site, and will add an is_active class and an aria-current="page" for improving accessibility signposting too.

{{ ' is_active' | contains(page.url, [item.url]) }}

This logic translates as "Output ' is_active' if the current nav item's URL contains the URL for the page we're currently on". We can do a similar thing for the aria-current attribute:

<a href="pageurl" {{ 'aria-current="page"' | contains(page.url, [item.url]) }}>Link text</a>

Or if you want it read a little nicer, we could use a standard Nunjucks {% if %} and set a variable earlier in our code:

{% set activePage = true | contains(page.url, [entry.url]) %}

<a
    href="pageurl"
    class="{% raw %}{% if activePage %} is_active{% endif %}{% endraw %}"
    {% if activePage %}aria-current="page"{% endif %}
>
    {{ entry.title }}
</a>

console

Sometimes when you're developing locally you'll run into weirdness. Sometimes it's because you've done something very stupid, misspelled something or missed something somewhere for some stupid reason. Sometimes, it is just because you didn't input the correct value, or check for the right key.

Whatever the reason, Nunjucks provides a filter called dump which lets you just output the raw values of things. Sounds great, and it's very useful until you try to output the sum total of everything in your navigation loop or something equally as mental.

At this point, you'll often run into some sort of JSON loop error that doesn't really tell you what's going on. Step forward console. This lets you output much more and much easier to help with debugging. To use it, just add it the same as any other filter.

Hard to find help

It took me a while to find something that helped with things like this, and then I eventually found the answer I needed on the Eleventy GitHub issues pages.

To use this, you'll also need to import util at the top of your config file:

const util = require('util');

And then add the filter alongside the others:

eleventyConfig.addFilter('console', function(value) {
    const str = util.inspect(value);

    return `<div style="white-space: pre-wrap;">${unescape(str)}</div>`;
});

Piping safe helps to ensure you don't output anything unescaped and cause yourself even more head-smashing, face-palming moments. For example, let's say we want to debug our new global navigation output. We can pipe each item in the loop through the console filter to get all the available data dumped to screen for us:

{%- for entry in pages %}
    {{ entry | console | safe }}
{% endfor %}

Which will output the following per item on the frontend for you to inspect:

<div style="white-space: pre-wrap;">{
  key: 'About us',
  order: 2,
  url: '/about/',
  pluginType: 'eleventy-navigation',
  title: 'About us',
  children: []
}</div>