Pagination
Pagination in Alloy serves two purposes with one mechanism: generating paginated list pages and creating virtual pages from data. The difference is perPage.
---
pagination:
data: collections.articles
perPage: 10
as: articles
permalink: "/articles/"
---
perPageomitted or1– One page per item (virtual pages). This is the default.perPage > 1– Items chunked into groups, one output page per chunk (paginated list).
Paginated list pages
A paginated list splits a collection across multiple output pages:
# content/articles.md
---
layout: article-list
pagination:
data: collections.articles
perPage: 10
as: articles
permalink: "/articles/"
---
{% for article in articles %}
<h2><a href="{{ article.url }}">{{ article.title }}</a></h2>
<p>{{ article.summary }}</p>
{% endfor %}
{% if pagination.previousPage %}
<a href="{{ pagination.previousPage }}">Previous</a>
{% endif %}
{% if pagination.nextPage %}
<a href="{{ pagination.nextPage }}">Next</a>
{% endif %}
With 47 articles and perPage: 10, Alloy generates 5 pages:
| URL | Content |
|---|---|
/articles/ |
Articles 1–10 (first page, no segment) |
/articles/page/2/ |
Articles 11–20 |
/articles/page/3/ |
Articles 21–30 |
/articles/page/4/ |
Articles 31–40 |
/articles/page/5/ |
Articles 41–47 |
The page path segment (page) is configured globally:
# alloy.config.yaml
pagination:
path: "page" # default -- /articles/page/2/
Change the segment word to match your locale or preference: path: "p" produces /articles/p/2/, path: "seite" produces /articles/seite/2/. The first page always outputs at the base permalink with no segment.
Virtual pages from data
When perPage is omitted or set to 1, Alloy creates one output page per item. This turns a single template and a data source into many pages:
# content/team.md
---
layout: default
pagination:
data: site.data.team
as: member
permalink: "/team/{{ member.slug }}/"
---
<h1>{{ member.name }}</h1>
<p>{{ member.role }}</p>
<img src="{{ member.photo }}" alt="{{ member.name }}">
Given data/team.yaml with 20 entries, this produces 20 pages at /team/alice/, /team/bob/, and so on. No individual content files needed.
Data sources
The data field accepts a dot-path reference to any available data:
| Path | Source |
|---|---|
site.data.team |
data/team.yaml |
site.data.products |
data/products.json |
collections.blog |
Blog section collection |
collections.articles |
Articles section collection |
The as variable
The as field names the variable that holds the current item (for virtual pages) or the current chunk (for list pages):
pagination:
data: site.data.team
as: member # {{ member.name }}, {{ member.slug }}
For list pages with perPage > 1, the as variable is an array:
pagination:
data: collections.articles
perPage: 10
as: articles # {% for article in articles %}
Pagination context
Every paginated page receives a pagination object in its template context:
{{ pagination.pageNumber }} -- Current page number (1-based)
{{ pagination.totalPages }} -- Total page count
{{ pagination.previousPage }} -- URL of previous page (nil if first)
{{ pagination.nextPage }} -- URL of next page (nil if last)
{{ pagination.first }} -- URL of first page
{{ pagination.last }} -- URL of last page
{{ pagination.items }} -- Items on the current page
The as variable is an alias for pagination.items. Both refer to the same data.
Front matter interpolation
When generating virtual pages (perPage: 1), string-valued front matter fields containing {{ }} or {% %} are interpolated using the pagination item context. This gives each virtual page its own title, description, and other metadata:
---
title: "{{ member.name }}"
heading: "About {{ member.name | upcase }}"
description: "Profile page for {{ member.name }}"
layout: default
pagination:
data: site.data.team
perPage: 1
as: member
permalink: "/team/{{ member.slug }}/"
---
For a team member { name: "Alice", slug: "alice" }, the virtual page gets:
page.title="Alice"page.heading="About ALICE"page.description="Profile page for Alice"page.url="/team/alice/"
Interpolation rules
- Only string-valued fields are interpolated. Numbers, booleans, arrays, and maps are left unchanged.
- Only fields containing
{{ }}or{% %}are sent through the template engine. Fields without markers skip the renderer entirely. - The template context contains only the
as:variable (e.g.,{ member: item }).site.*,page.*, andcollections.*are not available during front matter interpolation. - Skipped fields:
permalink(already processed),layout,pagination, and any key starting with_. - Interpolation only applies when
perPageis1. List pages (perPage > 1) do not interpolate front matter because theas:variable is an array, not a single item.
Combining with external data
Virtual pages pair well with Data Files and external data sources. Fetch product data from an API, store it in data/, and generate a page per product:
# content/products.md
---
layout: product
pagination:
data: site.data.products
as: product
permalink: "/products/{{ product.slug }}/"
title: "{{ product.name }}"
---
<p>{{ product.description }}</p>
<p>Price: {{ product.price }}</p>
Lifecycle interaction
Pagination operates on the post-filtered collection. Lifecycle filtering (drafts, future dates, expired dates) runs first, then pagination chunks the remaining items. A collection of 47 articles with 3 drafts produces 5 pages of 10 in build mode (44 items) but may produce different counts in dev mode (47 items, drafts included).