Intermediate

Pagination

Splitting a big collection into numbered pages with Eleventy's built-in pagination feature.

2 min read

Live Implementation

This is the real pager from /blog/ — 7 posts, 3 per page. Click a number, it goes to a real page:

How It Works

Eleventy has pagination built in. Add a pagination block to a template’s front matter and point it at any array (usually a collection), and Eleventy runs that template once per chunk, generating a separate page each time.

pagination:
    data: collections.posts
    size: 3
    alias: posts

Inside the template, posts is now just the slice of posts for the current page, and a pagination object is available with everything you need to build page links: pagination.pageNumber (0-indexed), pagination.hrefs (every page’s URL, in order), and more. The blog listing on this site uses exactly this to become /blog/, /blog/2/, /blog/3/, and so on, with a computed permalink so page 1 lands at the clean /blog/ URL instead of /blog/1/.

The numbered links above aren’t a mockup. They’re built from the real post count using the same pager macro blog.njk uses, and they go to the real paginated pages.

Folder Structure

src/blog.njk                     ← pagination front matter + permalink logic
src/_includes/components/pager.njk ← the numbered link list
src/_includes/demo-live/pagination.njk

Important Files

src/blog.njk

---
title: "Blog"
description: "Notes on building this site, and on Eleventy in general."
eleventyNavigation:
  key: Blog
  order: 3
pagination:
  data: collections.posts
  size: 3
  alias: posts
eleventyComputed:
  permalink: "{% if pagination.pageNumber == 0 %}/blog/{% else %}/blog/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
{% extends "layouts/base.njk" %}
{% import "components/pager.njk" as pagerC %}

{% block content %}
<div class="wrapper">
  <header class="page__header">
    <h1 class="page__title">{{ title }}</h1>
    <p class="page__lede">{{ description }}</p>
  </header>

  <ul class="post-list">
    {% for post in posts %}
    <li class="post-list__item">
      <a class="post-list__title" href="{{ post.url }}">{{ post.data.title }}</a>
      <time class="post-list__date" datetime="{{ post.date | htmlDateString }}">{{ post.date | readableDate }} · {{ post.date | timeAgo }}</time>
      <p class="post-list__desc">{{ post.data.description }}</p>
    </li>
    {% endfor %}
  </ul>

  {{ pagerC.pager(pagination.pageNumber, pagination.hrefs) }}
</div>
{% endblock %}

src/_includes/components/pager.njk

{% macro pager(currentIndex, hrefs) %}
{% if hrefs.length > 1 %}
<nav class="pager" aria-label="Pagination">
  <ol class="pager__list">
    <li>
      {% if currentIndex > 0 %}
      <a class="pager__step" href="{{ hrefs[currentIndex - 1] }}">← Previous</a>
      {% endif %}
    </li>
    {% for href in hrefs %}
    <li>
      <a href="{{ href }}" {% if loop.index0 == currentIndex %}aria-current="page"{% endif %}>{{ loop.index }}</a>
    </li>
    {% endfor %}
    <li>
      {% if currentIndex < hrefs.length - 1 %}
      <a class="pager__step" href="{{ hrefs[currentIndex + 1] }}">Next →</a>
      {% endif %}
    </li>
  </ol>
</nav>
{% endif %}
{% endmacro %}

Notes

  • By default, only the first paginated page gets added to collections.all, which is why /blog/ shows up once in the header nav instead of three times.
  • pagination.hrefs already has every page’s URL computed for you. There’s no need to build the list of links by hand.
  • Page size is just a number. Bump size: 3 up or down and the page count adjusts on its own.