codeflood logo

Eleventy Nested Pagination

Eleventy (the cool static site generator) has a great feature called Pagination which allows you to iterate over a list of things in chunks and produce multiple output files. As I was migrating this very site over to Eleventy, I found I needed to perform a double pagination. This is when, during paginating over a list of blog post years, I found I wanted to also paginate again over the posts in those years, so I could have at most 20 posts on a single list page.

Pagination of years and posts

In my description of the pagination feature above I say "in chunks" because you can specify the number of elements from the input list which should be grouped together for a single output file. So instead of generating an output file per list element, you could use groups of 20 elements and then for every output file the template is passed 20 elements at a time from the input list. For my scenario I needed to use pagination to both iterate single elements in a list (the blog post years) and also group up the nested pagination into groups of 20 so the blog post year listing pages would only have at most 20 blog posts each.

I tried a variety of combinations, but I couldn't get nested pagination to work. This is because pagination is only designed to iterate over a single flat list. But that list can contain elements of any type. It could be a list of primitives (numbers) like the years of a blog posting, it could be objects like a full blog post, or it could be a list of lists, like my blob posts grouped by year and posts in a year grouped into lists of 20 posts per list.

Conceptually what I wanted to iterate over looked something like this:

{
    "2012": [
        [
            // page 1
            {
                // post 1
            },
            {
                // post 2
            }
        ],
        [
            // page 2
            {
                // post 3
            },
            {
                // post 4
            }
        ]
    ],
    "2013": [
        [
            // page 1
            {
                // post 5
            },
            {
                // post 6
            }
        ],
        [
            // page 2
            {
                // post 7
            },
            {
                // post 8
            }
        ]
    ]
}

Structuring the data

So the first thing that needs to be done is to use the Collection API to define a collection for this data. I'll add this into my eleventy.config.js file:

module.exports = function(eleventyConfig) {
    eleventyConfig.addCollection("_postsByYear", collectionApi => {
        return collectionApi.getFilteredByTag("_blog");
    });
}

On my Eleventy site, I have several different "page" types. So I tag all my blog posts with the tag _blog (by using a directory data file) so I can easily filter down to only my blog posts for this kind of thing. All the above is doing is returning the full list of blog posts. I need to update the function to group the posts by year, and then group the post within each year into lists of maximum size 20.

Let's start with the grouping by year:

let postsByKey = {};
collectionApi.getFilteredByTag("_blog").forEach(post => {
    // Use the full year of the post date (like 2012) as the key to group by.
    let key = post.date.getFullYear();

    // Add the key to the postsByKey object if it doesn't exist and set the value as an empty array.
    if(!postsByKey[key]) {
        postsByKey[key] = [];
    }

    // Add the post to the proper year array.
    postsByKey[key].push(post);
});

Once this code runs the postsByKey variable will contain the posts grouped by year and would look something like:

{
    "2012": [
        {
            // post 1
        },
        {
            // post 2
        },
        {
            // post 3
        },
        {
            // post 4
        }
    ],
    "2013": [
        {
            // post 5
        },
        {
            // post 6
        },
        {
            // post 7
        },
        {
            // post 8
        }
    ]
}

Now to group the posts into smaller lists. I'm using the lodash NPM package to do the post grouping:

let postsByKeyPaged = [];
for(let key in postsByKey) {
    // Sort the posts in descending date order, so newest posts are first.
    postsByKey[key].sort((a, b) => a.date > b.date).reverse();

    // To generate the numbered page links we need to know the total number of pages.
    let totalPages = Math.ceil(postsByKey[key].length / 20);

    // Group the posts by using the 'chunk' function of lodash.
    lodash.chunk(postsByKey[key], 20).forEach((posts, index) => {
        // 'posts' here is an array of blog posts with a maximum length of 20.
        // Add each output page to the 'postsByKeyPaged' variable.
        postsByKeyPaged.push({
            key: key,
            posts: posts,
            pageNumber: index + 1,
            totalPages: totalPages
        });
    })
}

After this code runs the postsByKeyPaged variable holds an array of individual blog-posts-by-year pages. It's a single dimension (flat) array with each element containing the data required to construct a single page of the output. It looks something like:

[
    {
        "key": "2012",
        "posts": [
            {
                // post 1
            },
            {
                // post 2
            }
        ],
        "pageNumber": 1,
        "totalPages": 2
    },
    {
        "key": "2012",
        "posts": [
            {
                // post 3
            },
            {
                // post 4
            }
        ],
        "pageNumber": 2,
        "totalPages": 2
    },
    {
        "key": "2013",
        "posts": [
            {
                // post 5
            },
            {
                // post 6
            }
        ],
        "pageNumber": 1,
        "totalPages": 2
    },
    {
        "key": "2013",
        "posts": [
            {
                // post 7
            },
            {
                // post 8
            }
        ],
        "pageNumber": 2,
        "totalPages": 2
    }
]

Notice how the blog post years (key property) is repeated on each object in the output array? The main trick in making this approach work was to flatten the array and get rid of the blog post years as the top level object to paginate over. Instead, we'll be paginating over the pages we want to output.

Putting the entire function together for clarity:

module.exports = function(eleventyConfig) {
    eleventyConfig.addCollection("_postsByYear", collectionApi => {
        let postsByKey = {};
        collectionApi.getFilteredByTag("_blog").forEach(post => {
            let key = post.date.getFullYear();

            if(!postsByKey[key]) {
                postsByKey[key] = [];
            }

            postsByKey[key].push(post);
        });

        let postsByKeyPaged = [];
        for(let key in postsByKey) {
            postsByKey[key].sort((a, b) => a.date > b.date).reverse();

            let totalPages = Math.ceil(postsByKey[key].length / 20);

            lodash.chunk(postsByKey[key], 20).forEach((posts, index) => {
                postsByKeyPaged.push({
                    key: key,
                    posts: posts,
                    pageNumber: index + 1,
                    totalPages: totalPages
                });
            })
        }

        return postsByKeyPaged;
    });
}

Rendering the output files

We can now paginate over the _postsByYear collection. I'm using Nunjucks for the template doing the pagination:

---
pagination:
  data: collections._postsByYear
  size: 1
  alias: item
permalink: /blog/{{ item.key | slugify }}/{% if item.pageNumber > 1 %}page/{{ item.pageNumber }}/index.html{% endif %}
layout: "layouts/blog.njk"
---
<h1>Posts from {{ item.key }}</h1>
{% for post in item.posts %}
    <article>
        <h1 itemprop="name">
            <a class="archive-article-title" href="{{ post.url }}">{{ post.data.title }}</a>
        </h1>
        <p>
            {{ post | excerpt | safe }}
        </p>
    </article>
{% endfor %}

{% if item.totalPages > 1 %}
<div class="post-list-pager">
    <span>Page: </span>
{% for i in range(0, item.totalPages) -%}
    <span>
        {%- if (i + 1) != item.pageNumber %}
        <a href="/blog/{{ item.key }}/{% if i != 0 %}page/{{ i + 1}}{% endif %}">{{ i + 1 }}</a>
        {%- else %}
        {{ i + 1 }}
        {%- endif %}
    </span>
{%- endfor %}
</div>
{% endif %}

The URLs in the above template look a bit complex because I want my pages to be 1 based and not 0 based; I want the first page to be 1 and not 0.

Hopefully this approach will help some others who stumble into the same problem I did.

References

Thanks to the awesome Eleventy community, there were a few resources that helped me solve this issue:

Comments

Leave a comment

All fields are required.