Paginating a Jekyll Blog: What jekyll-paginate Does, and Where It Bites
jekyll-paginate splits a Jekyll blog feed into /page2/, /page3/ with two config keys — but the pages it builds are snapshots, not stable permalinks.
06 May 2026, 04:35 UTC

Your Jekyll blog's front page has quietly turned into an archive. Forty posts, full content each, all rendered into a single index.html — and every new publish makes it heavier. The standard fix is jekyll-paginate, the plugin that slices the blog feed into /page2/, /page3/ and so on. The takeaway before any setup: it needs two config keys and one template, but the pages it generates are build-time snapshots whose contents shift every time you publish. Treat them as navigation, never as permalinks.
Two config keys and a file literally named index.html
jekyll-paginate is an offset paginator. At build time it takes site.posts, sorts them (reverse-chronological by default), cuts the list into chunks of N, and writes one HTML page per chunk. The whole feature is configured with two keys in _config.yml:
plugins:
- jekyll-paginate
paginate: 5
paginate_path: "/blog/page:num/"
Three constraints cause most of the confusion, so they're worth stating plainly:
- The file must be named
index.html. Notindex.md, not a differently named template. Rename it and pagination silently stops. - It only paginates
site.posts. Categories, tags, and custom collections are out of scope for the core plugin. paginate_pathmust contain the:numplaceholder, and the index file must sit at the directory that path is relative to. With/blog/page:num/, that'sblog/index.htmlin your source tree.
Two more failure modes look like template bugs but aren't: setting paginate: 0 (or omitting paginate_path) disables pagination without an error, and the paginator Liquid object is only populated inside the paginated index file — reference it from an unrelated layout and you'll get nothing.
A worked example: five posts per page
Assume a source tree with 23 posts and blog/index.html. These notes assume Jekyll 4.x with jekyll-paginate 1.x; behavior has been stable across those lines, but check your locked gem versions if anything differs. The index template reads the current slice from the paginator object:
---
layout: default
---
{% for post in paginator.posts %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<p>{{ post.date | date: "%b %-d, %Y" }}</p>
</article>
{% endfor %}
<nav>
{% if paginator.previous_page_path %}
<a href="{{ paginator.previous_page_path }}">Newer</a>
{% endif %}
<span>Page {{ paginator.page }} of {{ paginator.total_pages }}</span>
{% if paginator.next_page_path %}
<a href="{{ paginator.next_page_path }}">Older</a>
{% endif %}
</nav>
Build from the project root, where the Gemfile lives (you need write access to _site/):
bundle exec jekyll build
Then check the output rather than trusting the browser:
_site/blog/page2/index.htmlexists and contains posts 6–10 in reverse-chronological order.paginator.total_pagesrenders as 5 — with 23 posts at 5 per page, 23 ÷ 5 rounds up to 5. A mismatch means the config didn't take.- Page 1 is served from
blog/index.htmlitself; there is nopage1directory, and on page 2 the "Newer" link points back to/blog/. - If
_sitecontains no page directories at all, work the checklist: gem listed in theGemfileand thepluginsarray,paginategreater than zero,:numpresent inpaginate_path, file namedindex.html.
The trade-off: page URLs that refuse to sit still
Because pagination is computed at build time from offsets, page 2 is "posts 6–10" only until the next publish. Add one post and page 2 becomes posts 7–11; every slice down the line shifts. Two practical consequences follow:
- Never link to paginator URLs from your own content. A "start here" link to
/blog/page3/will point at different posts next month. Link to post permalinks or dedicated archive pages instead. - Cache lifetimes should reflect the churn. A CDN can cache paginator pages fine, but a long TTL will serve stale slices after a deploy unless you purge them. Long caches belong on post permalinks; paginator pages want short ones.
There's also a scope limit: no filtering. The core plugin can't produce per-tag or per-category feeds, and custom ordering beyond site.posts' default sort requires extra work.
Paginate, paginate-v2, or client-side paging?
| Option | Paginates | Runs where | Good fit |
|---|---|---|---|
| jekyll-paginate | site.posts only | Anywhere Jekyll builds, including hosts with plugin allowlists | The main blog feed |
| jekyll-paginate-v2 | Posts, collections, tags, categories | Requires building yourself and deploying static output; not in GitHub Pages' default allowlist | Tag and category archives |
| Client-side JS paging | Anything already rendered on the page | In the browser at view time | Small archives where one stable URL matters |
One caveat deserves emphasis: GitHub Pages has historically allowlisted jekyll-paginate but not jekyll-paginate-v2, which is why the older plugin persists. Allowlists change over time, so confirm what your deployment target supports today before committing — this is the claim in this post most worth re-checking against current documentation.
A sensible default
Keep jekyll-paginate for the main feed — one gem, two config keys, and it builds anywhere Jekyll does. Generate per-tag archive pages separately (a loop over site.tags producing one page per tag, or collections) rather than fighting the core plugin's scope. And keep paginator URLs out of anything durable: navigation you consider canonical, sitemaps, external documentation.
Before you deploy, run one build and verify the three things above: the page directories exist in _site, the total page count matches your post math, and page 2 holds the posts you expect. Ten seconds of checking beats a silent paginate: 0 discovered in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.