Rift includes a built-in pagination utility that makes it easy to split large content arrays (such as blog posts, articles, or product lists) into multiple pages. This feature is used directly inside controller files to drive paginated output generation.
The paginate(array, pageSize) function accepts any array of items and returns an array of page
objects. Each page includes metadata such as the index, start offset, previous/next references, and a slice
of the items.
You call paginate() inside your controller's params() function to divide content
into multiple pages. Then, for each page, you call ctx.param({ page }) to register it.
Here's how you can paginate a list of posts and generate separate pages with their own URLs:
export async function params(ctx) {
const posts = ctx.collections.posts.filter(p => p.locale === ctx.locale);
const pages = paginate(posts, 10);
for (const page of pages) {
ctx.param({ page });
}
}
export function permalink(ctx) {
const pageNum = ctx.params.page.index;
return pageNum === 0
? `/${ctx.locale}/blog/`
: `/${ctx.locale}/blog/page/${pageNum + 1}`;
}
This example creates:
/en/blog/ – the first page of posts/en/blog/page/2, /en/blog/page/3, etc. – subsequent pages
Inside your template, you can access page.items to render the current chunk, and use
page.previous / page.next to create navigation links.
In your template (e.g. blog listing), loop through page.items and render each item. You can
also conditionally render previous/next navigation using page.previous and
page.next.
{% for post in page.items %}
<h2>{{ post.title }}</h2>
<p>{{ post.summary }}</p>
{% endfor %}
<nav>
{% if page.previous != null %}
<a href="/blog/page/{{ page.previous + 1 }}">Previous</a>
{% endif %}
{% if page.next != null %}
<a href="/blog/page/{{ page.next + 1 }}">Next</a>
{% endif %}
</nav>
You can also paginate blog posts for each category:
export async function params(ctx) {
const posts = ctx.collections.posts.filter(p => p.locale === ctx.locale);
const categories = [...new Set(posts.map(p => p.category))];
for (const category of categories) {
const postsInCategory = posts.filter(p => p.category === category);
const pages = paginate(postsInCategory, 10);
for (const page of pages) {
ctx.param({ category, page });
}
}
}
export function permalink(ctx) {
const index = ctx.params.page.index;
const base = `/${ctx.locale}/blog/category/${ctx.params.category}`;
return index === 0 ? base : `${base}/page/${index + 1}`;
}
This will generate pages like:
/en/blog/category/javascript/en/blog/category/javascript/page/2/en/blog/category/css
In your template (e.g. blog listing), loop through page.items and render each item. You can
also conditionally render previous/next navigation using page.previous and
page.next.
{% for post in page.items %}
<h2>{{ post.title }}</h2>
<p>{{ post.summary }}</p>
{% endfor %}
<nav>
{% if page.previous != null %}
<a href="/blog/page/{{ page.previous + 1 }}">Previous</a>
{% endif %}
{% if page.next != null %}
<a href="/blog/page/{{ page.next + 1 }}">Next</a>
{% endif %}
</nav>
For user-friendly navigation, you can display a pagination window that shows the current page number alongside nearby pages:
In your controller:
const pages = paginate(posts, 10);
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const window = pages.slice(Math.max(i - 2, 0), i + 3);
ctx.param({ page, window });
}
In your template:
<nav class="pagination">
{% if page.previous != null %}
<a href="/blog/page/{{ page.previous + 1 }}">« Previous</a>
{% endif %}
{% for p in window %}
{% set pageNum = p.index + 1 %}
{% if p.index == page.index %}
<strong>{{ pageNum }}</strong>
{% else %}
<a href="/blog/page/{{ pageNum }}">{{ pageNum }}</a>
{% endif %}
{% endfor %}
{% if page.next != null %}
<a href="/blog/page/{{ page.next + 1 }}">Next »</a>
{% endif %}
</nav>
This displays a range like below