Search code examples
javascriptloopssortingnunjuckseleventy

How do you sort a list of blog post tags by the number of posts that contain the tag (using Nunjucks in Eleventy)?


I have a blog built with Eleventy (static site generator), using Nunjucks as the templating language.

I have a page that lists all the tags I've assigned to my posts. It lists them in alphabetical order, with the number of posts per tag.

What I'd also like to do is list the tags in order of frequency (most-used tags first).

The code that works to get the alphabetical list (with counts) looks like this:

<section id="tags-number-of-posts">
  {% for tag2, posts2 in collections | dictsort %}
  {% set tag2Url %}/tags/{{ tag2 | slug }}/{% endset %}
   <a href="{{ tag2Url | url }}">{{ tag2 }} ({{ posts2 | length }})</a><br/>
  {% endfor %}
</section>

The result now is like:

Blender (1)
boats (2)
Bomb Factory (1)
bonfires (4)
books (3)

but I'd like it to be:

bonfires (4)
books (3)
boats (2)
Blender (1)
Bomb Factory (1)

(You can see the actual result at this deploy preview site: https://text-timeline--davidrhoden-basic.netlify.app/tagslist/ .)

I've tried changing dictsort to sort(attribute="posts2.length"), as well as other permutations that might make sense, like sort(attribute="length"), but nothing has worked.

I guess "length" is not an attribute of the posts themselves. So maybe a filter like sort won't work in this context.

But is there a way to sort this list by those post counts? Surely there must be. Do I need to bring in something like lodash, or some javascript function like map?


Solution

  • You might be able to use Eleventy custom collections to do what you want. We can use Javascript in the .eleventy.js file to count the number of posts in each tag, then sort the data by the number of posts.

    Since Eleventy doesn't seem to give us a pre-grouped object of tags and posts, we're doing that ourselves. This does mean that if you put duplicate tags on one post, it will be counted twice. It is possible to de-dupe the tags, but it shouldn't be an issue if you're careful.

    // .eleventy.js
    module.exports = function (eleventyConfig) {
      // ...
    
      // addCollection receives the new collection's name and a
      // callback that can return any arbitrary data (since v0.5.3)
      eleventyConfig.addCollection('bySize', (collectionApi) => {
        // see https://www.11ty.dev/docs/collections/#getall()
        const allPosts = collectionApi.getAll()
    
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
        const countPostsByTag = new Map()
        allPosts.forEach((post) => {
          // short circuit eval sets tags to an empty array if there are no tags set
          const tags = post.data.tags || []
          tags.forEach((tag) => {
            const count = countPostsByTag.get(tag) || 0
            countPostsByTag.set(tag, count + 1)
          })
        })
    
        // Maps are iterators so we spread it into an array to sort
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
        const sortedArray = [...countPostsByTag].sort((a, b) => b[1] - a[1])
        
        // this function returns an array of [tag, count] pairs sorted by count
        // [['bonfires', 4], ['books', 3], ['boats', 2], ...]
        return sortedArray
      })
      })
    
      return {
        // ...
      }
    }
    

    We can then use this data in Nunjucks with collections.bySize.

    <section id="tags-number-of-posts">
        {# we can still destructure the tag name and count even though it's an array #}
        {% for tag, count in collections.bySize %}
            {% set tagUrl %}/tags/{{ tag | slug }}/{% endset %}
            <a href="{{ tagUrl | url }}">{{ tag }} ({{ count }})</a><br/>
        {% endfor %}
    </section>
    

    If you need to have the array of posts in your collections object and not just the number of posts, it is also possible to modify the JavaScript to do so.