Pagination

Pagination divides one ordered collection into several rendered views. An ordinary authored listing declares [pagination]; a taxonomy term listing uses the per_page value from its taxonomy definition. Neither form discovers pages, changes collection order, or lets a template create output.

Start with the collection guide or the tutorial's pagination step if the collection model is new to you.

A paginated listing

First define an ordered collection in heine.toml:

[collections.posts]
order = "published-desc"

Then make an ordinary page a listing:

# content/en/blog/index.page
template = "section-index.tera"
title = "Blog"
# Keep this listing page itself out of the posts collection.
collections = []

[pagination]
collection = "posts"
per_page = 10

[pagination.navigation]
window = 1

collection and per_page are required. per_page must be positive. The listing page itself is page one; Heine renders later pages only when the collection has more members. An empty collection still renders its authored listing once.

Collection order remains the single source of ordering. A listing cannot filter, reverse, or sort its collection.

Taxonomy term listings use the same pagination view and pagination_links() function. Their generated page-one route is the literal term route, and later pages use path/<term>/page/<number>.html; their compact navigation window is 1.

Series listings also use this view. Their route segment and navigation window are fixed by the Series contract rather than an authored [pagination] table.

Routes

Later pagers use the page route segment by default. For an authored listing at blog/index.html, they are blog/page/2.html, blog/page/3.html, and so on. For archive.html, they are archive/page/2.html, archive/page/3.html, and so on. There is no duplicate page/1 output.

Change the segment only when a site has a concrete routing need:

[pagination]
collection = "posts"
per_page = 10
segment = "older"

segment must use ASCII letters or digits, or -._~!$&'()*+,;=@; it cannot be . or .., end in ., or contain percent escapes. Generated paths are both URLs and filesystem names, so use older-posts rather than older%20posts. Generated routes use the listing's configured suffix, locale prefix, and site base_path, and they participate in normal output-collision checks before anything is written.

Template values

pagination is null while rendering an ordinary page. A paginated listing receives this value:

FieldMeaning
collectionconfigured collection name
pagesrender-facing page views in the current slice, each with url and pager-relative relative_url
item_counttotal collection members
per_pageconfigured slice size
first_item_numberone-based number of the first current-slice item, or 0 for an empty listing
page_numbercurrent one-based pager number
page_counttotal pager count; at least one
current, first, lastpager link views
previous, nextadjacent pager link views, or null
navigationcompact link and gap items for ordinary navigation

A pager link has page_number, url, relative_url, and current fields. The URL fields have the same base-path and relative-link behavior as Heine's other resource views.

{% if pagination %}
  {% for post in pagination.pages %}
    <article>
      <h2><a href="{{ post.relative_url }}">{{ post.title }}</a></h2>
      {% if post.summary %}<p>{{ post.summary }}</p>{% endif %}
    </article>
  {% endfor %}

  <nav aria-label="Pagination">
    {% for item in pagination.navigation %}
      {% if item.kind == "gap" %}
        <span aria-hidden="true">…</span>
      {% elif item.current %}
        <span aria-current="page">{{ item.page_number }}</span>
      {% else %}
        <a href="{{ item.relative_url }}">{{ item.page_number }}</a>
      {% endif %}
    {% endfor %}
  </nav>
{% endif %}

The authored page remains the listing page for every pager. Use pagination.current where a template needs the generated page's URL.

[pagination.navigation].window defaults to 1 and accepts values from 1 through 5. It preserves that many adjacent links beside the first page, current page, and last page. Overlapping ranges merge before gaps are added.

For pager 101 of 1,202:

window = 1:  1 2 … 100 101 102 … 1201 1202
window = 2:  1 2 3 … 99 100 101 102 103 … 1200 1201 1202

Small listings therefore show every pager once: two pagers always yield 1 2, never repeated links or ellipses.

For a deliberately complete sequence, call pagination_links() inside a paginated listing template. It returns every pager link, without gap items:

{% for link in pagination_links() %}
  {% if link.current %}
    <span aria-current="page">{{ link.page_number }}</span>
  {% else %}
    <a href="{{ link.relative_url }}">{{ link.page_number }}</a>
  {% endif %}
{% endfor %}

This function is explicit because a complete sequence can be large. Calling it outside a paginated listing is a template call-site error.

Build behavior

Heine resolves paginated listings after collections and before template rendering. Every generated pager is an output task, not an authored page: it has no page ID and does not appear in page(), collections, siblings, or translations.

Full builds check every generated route and render every pager before publishing. Quick builds re-render all pagers of a listing when a present member page changes. As elsewhere, quick builds intentionally do not detect renames, deletions, template changes, configuration changes, or changed relationships; use a full build for those changes.