Possum websites, done possumly

Part 7: Category pages

6 minute read time / 740 words

We've got our basic pagination working, and we're outputting our tags on our post listings page, but they're not that useful at the moment.

Updating the single article page

We've already updated our post listings page to output tags, the date and datetime, and a page description to make those listings more useful and informative. Our individual article pages are still a bit barebones. The least we can do is add the date and tags to them to bring them inline with our post listings.

Open up the article.njk template. It's a bit basic at the moment. Let's add some niceties to it:

---
layout: base.njk
---
<time datetime="{{ date | niceDateTime }}">
    {{ date | niceDate }}
</time>

{{ content | safe }}

<h4>Tagged as:</h4>

<ul class="tags">
    {%- for tag in categories -%}
    <li class="tag tag--{{ tag | lower | replace(' ', '') }}">{{ tag }}</li>
    {%- endfor -%}
</ul>

Now we're going to get a proper date and a list of tags output, just like on the listings page. But we can definitely do better than this. Let's get some category listing pages sorted so we can filter by tags to see articles on that topic.

Creating category pages

We're going to need a way to get just posts tagged with a specific category if we are going to make this work. Luckily, custom filters to the rescue again! Open up our config file and add the new custom filter:

eleventyConfig.addFilter('filterByCategory', function(posts, cat) {
    cat = cat.toLowerCase();

    let result = posts.filter(p => {
        let cats = p.data.categories.map(s => s.toLowerCase());

        return cats.includes(cat);
    });

    return result;
});

We're also going to need to configure Eleventy to create dymanic collections for us based on what tags are assigned to posts. To do this, we can add the following to our config file:

eleventyConfig.addCollection('categories', function(collectionApi) {
    let categories = new Set();
    const posts = collectionApi.getFilteredByTag('article');

    posts.forEach(p => {
        let cats = p.data.categories;
        cats.forEach(c => categories.add(c));
    });

    return Array.from(categories);
});

Now, create a new file: /src/posts/categories.njk and add this code:

---
layout: base.njk
eleventyComputed:
    title: "Posts tagged: '{{ category }}'"
pagination:
  data: collections.categories
  size: 1
  alias: category
permalink: "/posts/tagged/{{ category | slugify }}/"
---
{% set articles = collections.article | filterByCategory(category) %}

<ul>
    {%- for article in articles  -%}
        <li>
            <h2>
                <a href="{{article.url}}">
                    {{ article.data.title }}
                </a>
            </h2>

            <time datetime="{{ article.date | niceDateTime }}">
                {{ article.date | niceDate }}
            </time>

            <p class="desc">
                {{ article.data.description }}
            </p>

            {% set tagsList = article.data.categories %}

            <h4>Tagged as:</h4>

            <ul class="tags">
                {%- for tag in tagsList -%}
                <li class="tag tag--{{ tag | lower | replace(' ', '') }}">{{ tag }}</li>
                {%- endfor -%}
            </ul>
        </li>
    {%- endfor -%}
</ul>
{% raw %}{% endhighlight %}

        <p>
            Perfect! Now we've got new category pages we can get to via <code class="language-html">/posts/tagged/tagname/</code>.
        </p>

        <h2 class="sub-title" id="linking-tags-to-categories">Linking tags to categories</h2>

        <p>
            But how do we know how to get to them? We're going to need to change our tag output on both the post listing and the article pages so that the tag name links to the correct category. This is straightforward enough to do now we know the structure of the URLs to the category pages.
        </p>

        <p>
            We need to update our tag output in <code class="language-html">posts.njk</code>, <code class="language-html">categories.njk</code> and <code class="language-html">article.njk</code> to look like this:
        </p>

{% highlight 'liquid' %}{% raw %}
<ul class="tags">
    {%- for tag in tagsList -%}
    <li>
        <a href="/posts/tagged/{{ tag | slugify }}/" class="tag tag--{{ tag | lower | replace(' ', '') }}">
            {{ tag }}
        </a>
    </li>
    {%- endfor -%}
</ul>
{% endraw %}

Go to a page using any of those three templates and your tags are now linked to a category page that will only show articles tagged with that category. Beautiful!

But there's obvious problems here: we've just pasted the same code into three templates. That's already a maintenance nightmare, so surely there's something we can do about that? Yes there is! In the next part of this guide, we'll look at includes and macros and how we can reduce maintenance and increase repeatability with ease.