How to Add Pagination to Your Jekyll Blog
Set up pagination in Jekyll using jekyll-paginate-v2 — split posts across multiple pages with numbered links, category filtering, and collection support.
When your Jekyll blog is young, showing every post on a single page is fine. But once you cross 20 or 30 posts, that page becomes a problem. It loads slowly, it’s hard to navigate, and search engines may not crawl deep into a single enormous listing page. Pagination solves all of this by splitting your posts across multiple pages with numbered links or previous/next controls.
This guide walks through everything you need to set up pagination in Jekyll properly — from choosing the right plugin to handling edge cases like category-filtered pages and custom collections.
Which pagination plugin should you use?
Jekyll ships with two pagination options, and the difference matters.
jekyll-paginate is the original, built-in plugin. It is simple to configure but severely limited: it only works on index.html, it can only paginate posts (not collections), and it cannot filter by category or tag. Jekyll has officially marked it as deprecated, meaning it will not receive new features or bug fixes. It still works, but you should not build new projects around it.
jekyll-paginate-v2 is the modern replacement. It supports any page (not just index.html), paginates posts, collections, and filtered subsets, supports numbered page links, and handles category/tag pagination cleanly. It requires a GitHub Actions build for GitHub Pages (it is not on the allowed plugins list), but for any other host — Netlify, Vercel, Cloudflare Pages — it works with a standard bundle exec jekyll build.
For everything in this guide, use jekyll-paginate-v2.
Setting up jekyll-paginate-v2
Step 1: Install the gem
# Gemfile
gem "jekyll-paginate-v2"
# _config.yml
plugins:
- jekyll-paginate-v2
pagination:
enabled: true
per_page: 10
permalink: '/page/:num/'
title: ':title - Page :num'
limit: 0 # 0 = no limit on total pages
sort_field: 'date'
sort_reverse: true
Run bundle install after adding the gem.
The permalink setting defines the URL structure for pages beyond the first. With /page/:num/, page 2 lives at /blog/page/2/ and page 3 at /blog/page/3/. The first page always lives at the base URL.
Step 2: Enable pagination on your blog index
In your blog listing page (typically blog.md or index.html), add pagination to the front matter:
---
layout: home
title: Blog
pagination:
enabled: true
---
That pagination: enabled: true in the page front matter is required in addition to the global config. Without it, the page will not paginate.
Step 3: Update your template to use the paginator
In your home layout (_layouts/home.html), use paginator.posts instead of site.posts:
{% if paginator.posts %}
{% assign posts = paginator.posts %}
{% else %}
{% assign posts = site.posts %}
{% endif %}
{% for post in posts %}
<article class="post-card">
<a href="{{ post.url }}">
<h2>{{ post.title }}</h2>
</a>
<time datetime="{{ post.date | date_to_xmlschema }}">
{{ post.date | date: "%B %d, %Y" }}
</time>
<p>{{ post.excerpt | strip_html | truncatewords: 30 }}</p>
<a href="{{ post.url }}" class="read-more">Read more →</a>
</article>
{% endfor %}
{% include pagination.html %}
The conditional if paginator.posts is important — it lets the layout work on pages that don’t use pagination, so you can reuse the same layout for filtered or static pages.
Step 4: Create the pagination include
Create _includes/pagination.html:
{% if paginator.total_pages > 1 %}
<nav class="pagination" aria-label="Blog pagination" role="navigation">
{% if paginator.previous_page %}
<a href="{{ paginator.previous_page_path | relative_url }}"
class="pagination__prev"
rel="prev"
aria-label="Previous page">
← Newer posts
</a>
{% else %}
<span class="pagination__prev pagination__prev--disabled" aria-hidden="true">
← Newer posts
</span>
{% endif %}
<span class="pagination__info" aria-current="true">
Page {{ paginator.page }} of {{ paginator.total_pages }}
</span>
{% if paginator.next_page %}
<a href="{{ paginator.next_page_path | relative_url }}"
class="pagination__next"
rel="next"
aria-label="Next page">
Older posts →
</a>
{% else %}
<span class="pagination__next pagination__next--disabled" aria-hidden="true">
Older posts →
</span>
{% endif %}
</nav>
{% endif %}
Notice the rel="prev" and rel="next" attributes. These help search engines understand the relationship between paginated pages and consolidate their crawl budget.
Step 5: Style the pagination
// _sass/_pagination.scss
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin: 2.5rem 0;
font-size: 0.95rem;
}
.pagination__prev,
.pagination__next {
display: inline-flex;
align-items: center;
padding: 0.5rem 1.25rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--link-color);
text-decoration: none;
font-weight: 500;
transition: background 0.15s, border-color 0.15s;
&:hover {
background: var(--card-bg);
border-color: var(--color-primary);
}
&--disabled {
color: var(--text-muted);
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}
}
.pagination__info {
color: var(--text-muted);
font-size: 0.875rem;
min-width: 8rem;
text-align: center;
}
Numbered page links
For sites with many pages, numbered links let readers jump directly to a page. Here is a complete numbered pagination bar:
{% if paginator.total_pages > 1 %}
<nav class="pagination" aria-label="Blog pages">
{% if paginator.previous_page %}
<a href="{{ paginator.previous_page_path | relative_url }}"
class="pagination__arrow" rel="prev" aria-label="Previous">‹</a>
{% else %}
<span class="pagination__arrow pagination__arrow--disabled" aria-hidden="true">‹</span>
{% endif %}
{% for i in (1..paginator.total_pages) %}
{% if i == paginator.page %}
<span class="pagination__page pagination__page--current" aria-current="page">{{ i }}</span>
{% elsif i == 1 %}
<a href="{{ paginator.first_page_path | relative_url }}" class="pagination__page">{{ i }}</a>
{% else %}
<a href="{{ site.paginate_path | replace: ':num', i | relative_url }}" class="pagination__page">{{ i }}</a>
{% endif %}
{% endfor %}
{% if paginator.next_page %}
<a href="{{ paginator.next_page_path | relative_url }}"
class="pagination__arrow" rel="next" aria-label="Next">›</a>
{% else %}
<span class="pagination__arrow pagination__arrow--disabled" aria-hidden="true">›</span>
{% endif %}
</nav>
{% endif %}
For blogs with more than ten pages, show only a window of pages around the current one rather than listing every page number. This keeps the navigation compact regardless of how many posts you have.
Paginating by category
One of the most useful features of jekyll-paginate-v2 is the ability to paginate a filtered set of posts. This is how you build a /tutorials/ page that only shows Tutorial posts, paginated:
---
layout: category
title: Tutorials
pagination:
enabled: true
category: Tutorial
per_page: 8
---
The category key filters posts to only those with category: Tutorial in their front matter. The paginator then works identically to the main blog, but only on the filtered set. You can do the same with tags:
---
layout: tag
title: "Posts tagged: jekyll"
pagination:
enabled: true
tag: jekyll
per_page: 10
---
Paginating collections
jekyll-paginate-v2 can paginate any Jekyll collection, not just posts. This is useful for themes, products, or documentation pages:
---
layout: themes
title: All Themes
pagination:
enabled: true
collection: themes
per_page: 12
sort_field: 'title'
sort_reverse: false
---
In the layout, use paginator.posts exactly as you would for blog posts — even though you are iterating over a collection, the paginator variable name stays the same.
Understanding the paginator object
The paginator object exposes all the information you need to build any pagination UI. The most useful variables are:
paginator.page— current page numberpaginator.total_pages— total number of pagespaginator.total_posts— total number of items being paginatedpaginator.per_page— items per page (from config)paginator.posts— the posts/items on the current pagepaginator.previous_page— page number of previous page (nil if on page 1)paginator.previous_page_path— URL of previous pagepaginator.next_page— page number of next page (nil if on last page)paginator.next_page_path— URL of next pagepaginator.first_page_path— URL of page 1paginator.last_page_path— URL of last page
You can use these to build a post count display, a progress indicator, or a “showing posts X–Y of Z” message:
{% assign from = paginator.page | minus: 1 | times: paginator.per_page | plus: 1 %}
{% assign to = from | plus: paginator.posts.size | minus: 1 %}
<p class="pagination__count">
Showing {{ from }}–{{ to }} of {{ paginator.total_posts }} posts
</p>
SEO and pagination
Pagination can affect your site’s search engine visibility if not handled correctly.
Use rel=prev/next. The rel="prev" and rel="next" link attributes in your pagination controls tell search engines the order of paginated content. This helps them understand which page is canonical and how to distribute link equity.
Do not noindex paginated pages. Some sites add noindex to pages 2 and beyond, thinking it prevents duplicate content issues. This is wrong — it prevents those pages from being crawled and indexed. Let paginated pages be indexed normally.
Set a canonical URL on page 1. Some SEO plugins automatically set a canonical tag pointing page 2+ back to page 1. This is only appropriate if the pages have very similar content; it is better to let each page be indexed on its own if your posts are sufficiently different.
Avoid thin paginated pages. If per_page is set very high (say 50 posts) and most of your pages show just one or two posts, consider reducing the per-page count to keep page content balanced.
Common problems and solutions
“Pagination does not work” on GitHub Pages
jekyll-paginate-v2 is not on GitHub Pages’ allowlist of safe plugins. You need to build with GitHub Actions and deploy the _site/ output:
# .github/workflows/deploy.yml
- name: Build with Jekyll
run: bundle exec jekyll build
env:
JEKYLL_ENV: production
- name: Deploy to GitHub Pages
uses: actions/upload-pages-artifact@v3
with:
path: _site/
Paginated pages return 404
Check that your permalink in _config.yml under pagination: matches the URL you expect. If you set permalink: '/page/:num/' and your blog is at /blog/, page 2 will be at /blog/page/2/ — not /page/2/.
Only page 1 exists — pages 2+ are missing
Make sure pagination: enabled: true appears in both the global _config.yml AND the specific page’s front matter. Both are required.
paginator is nil on my page
The paginator object is only available on pages that have pagination: enabled: true in their front matter. On all other pages, paginator is nil, so wrap any paginator usage in a {% if paginator %} check.
Performance tip: per_page and load time
Higher per_page values mean fewer pages but larger individual page payloads — especially if each post card includes an image. A per_page of 10–12 is a good balance for most blogs: enough context for the reader to browse without overwhelming the page’s HTML or image load.
If your post cards include loading="lazy" on images, higher per-page values have less impact on initial load time, since off-screen images are deferred automatically.
Pagination is one of those features that matters much more than it initially seems. A well-implemented paginator improves navigation, reduces page weight, and signals to search engines that your site has organised, deep content worth crawling. Many Jekyll themes on JekyllHub include pre-built pagination — look for themes that list “blog” or “pagination” in their features.
Pagination and SEO
Paginated pages require careful handling to avoid SEO problems. Each page in a pagination series (/blog/, /blog/page/2/, /blog/page/3/) is a distinct URL, and search engines treat them accordingly. Make sure your theme adds rel="canonical" on paginated pages pointing to the page itself — not to /blog/ — to avoid canonicalisation confusion.
If your post list appears on multiple URLs (for example, a category archive and the main blog list both using jekyll-paginate), use canonical tags to specify the definitive version of the content. The paginator’s page.url variable gives you the current page URL, which you can use to construct the canonical tag in your layout.
Internal linking between paginated pages also matters. A “Previous posts” and “Next posts” pagination nav, beyond improving user experience, helps search engines discover and crawl your full post archive. Many themes implement only a simple previous/next page link — if yours does not include the full page number navigation (1, 2, 3… N), consider adding it, particularly if you have a large post archive where deep pagination pages might otherwise not be crawled.
Finally, avoid the temptation to noindex pagination pages. The conventional wisdom that paginated pages should be excluded from search results is outdated — search engines have become quite good at understanding pagination sequences and distributing ranking signals appropriately across them. Index every paginated page and let the search engine handle the rest.
Pagination as part of a broader content architecture
Pagination works best when it is part of a thoughtful content architecture, not bolted on as an afterthought. Think about what pages you want indexed, how readers move between them, and what signals each paginated URL sends to search engines. A blog with twenty posts does not need pagination — but a blog with one hundred posts where readers cannot easily browse by topic benefits enormously from well-implemented pagination combined with a tag or category archive.
Combine jekyll-paginate-v2 with a category archive so readers can paginate within a specific topic. Add a “back to all posts” breadcrumb on individual posts so readers always have a path back to the paginated list. Link your sitemap to the first paginated page, not every page in the sequence — search engines will follow the next-page links from there. These small structural decisions, made once when you set up pagination, create a content architecture that serves both readers and search engines efficiently for the lifetime of your blog.