SSG Pagination — Splitting Content Across Multiple Pages
In this tutorial, you will learn about SSG Pagination. We cover key concepts, practical examples, and best practices to help you master this topic.
SSG pagination splits large content collections into multiple static pages with next/previous navigation, improving load time and user experience.
What You'll Learn
By the end of this tutorial, you'll understand how to implement pagination in major SSG frameworks, handle page numbering, create navigation components, and optimize pagination for SEO.
Why It Matters
A blog with 500 posts on a single page is unusable. Pagination breaks content into chunks, improves load time, and helps search engines index your content structure.
Real-World Use
A news site with 10,000 articles paginates by month and category. Each page shows 20 articles with numbered navigation. Google crawls each paginated page separately, and users navigate easily through years of content.
Pagination Architecture
graph TD
A[Content Collection
500 posts] --> B[Pagination Logic]
B --> C[Page 1
posts 1-10]
B --> D[Page 2
posts 11-20]
B --> E[Page 3
posts 21-30]
B --> F[... 50 pages total]
C --> G[Generated URLs
/blog/page/1]
D --> G
E --> G
F --> G
G --> H[Static HTML files]
H --> I[CDN Deploy]
I --> J[User navigates
between pages]
style B fill:#4a90d9,color:#fff
style H fill:#27ae60,color:#fff
style J fill:#e67e22,color:#fff
Next.js Pagination
// pages/blog/page/[page].js — Dynamic pagination
export default function BlogPage({ posts, currentPage, totalPages }) {
return (
<div>
<h1>Blog — Page {currentPage}</h1>
<div className="posts">
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<a href={`/blog/${post.slug}`}>Read more</a>
</article>
))}
</div>
<nav className="pagination">
{currentPage > 1 && (
<a href={`/blog/page/${currentPage - 1}`}>Previous</a>
)}
{Array.from({ length: totalPages }, (_, i) => i + 1)
.map(page => (
<a
key={page}
href={`/blog/page/${page}`}
className={page === currentPage ? 'active' : ''}
>
{page}
</a>
))
}
{currentPage < totalPages && (
<a href={`/blog/page/${currentPage + 1}`}>Next</a>
)}
</nav>
</div>
);
}
const POSTS_PER_PAGE = 10;
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts');
const allPosts = await res.json();
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
const paths = Array.from({ length: totalPages }, (_, i) => ({
params: { page: String(i + 1) },
}));
return { paths, fallback: false };
}
export async function getStaticProps({ params }) {
const page = parseInt(params.page);
const res = await fetch('https://api.example.com/posts');
const allPosts = await res.json();
const start = (page - 1) * POSTS_PER_PAGE;
const posts = allPosts.slice(start, start + POSTS_PER_PAGE);
return {
props: {
posts,
currentPage: page,
totalPages: Math.ceil(allPosts.length / POSTS_PER_PAGE),
},
};
}
Eleventy Pagination
---
# src/blog.njk — Eleventy paginated blog listing
pagination:
data: collections.posts
size: 5
alias: posts
reverse: true
permalink: "blog{% if pagination.pageNumber > 0 %}/{{ pagination.pageNumber + 1 }}{% endif %}/"
---
<h1>Blog</h1>
<div class="posts">
{% for post in posts %}
<article>
<h2><a href="{{ post.url }}">{{ post.data.title }}</a></h2>
<time>{{ post.date | readableDate }}</time>
<p>{{ post.data.description }}</p>
</article>
{% endfor %}
</div>
{# Pagination navigation #}
<nav class="pagination">
{% if pagination.previousPageHref %}
<a href="{{ pagination.previousPageHref }}" class="prev">Previous</a>
{% endif %}
{% set totalPages = pagination.pages.length %}
{% for pageEntry in pagination.pages %}
{% set pageNum = loop.index %}
<a href="{{ pagination.hrefs[loop.index0] }}"
class="{% if page.url == pagination.hrefs[loop.index0] %}active{% endif %}">
{{ pageNum }}
</a>
{% endfor %}
{% if pagination.nextPageHref %}
<a href="{{ pagination.nextPageHref }}" class="next">Next</a>
{% endif %}
</nav>
Hugo Pagination
<!-- layouts/_default/list.html — Hugo pagination -->
{{ define "main" }}
<h1>{{ .Title }}</h1>
<div class="posts">
{{ range .Paginator.Pages }}
<article>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
<time>{{ .Date.Format "Jan 2, 2006" }}</time>
<p>{{ .Summary }}</p>
</article>
{{ end }}
</div>
<!-- Pagination navigation -->
<nav class="pagination">
{{ if .Paginator.HasPrev }}
<a href="{{ .Paginator.Prev.URL }}" class="prev">Previous</a>
{{ end }}
{{ .Paginator.PageNumber }} / {{ .Paginator.TotalPages }}
{{ if .Paginator.HasNext }}
<a href="{{ .Paginator.Next.URL }}" class="next">Next</a>
{{ end }}
</nav>
<!-- Full page number navigation -->
<nav class="pagination-numbers">
{{ range .Paginator.Pagers }}
<a href="{{ .URL }}"
class="page {{ if eq .PageNumber $.Paginator.PageNumber }}active{{ end }}">
{{ .PageNumber }}
</a>
{{ end }}
</nav>
{{ end }}
<!-- config.toml — Pagination settings -->
[pagination]
pagerSize = 10
path = "page"
SEO for Paginated Pages
<!-- Add rel=next and rel=prev for SEO -->
<link rel="prev" href="https://example.com/blog/page/2/" />
<link rel="next" href="https://example.com/blog/page/4/" />
<!-- views/partials/seo-pagination.html -->
{{ if .Paginator }}
{{ if .Paginator.HasPrev }}
<link rel="prev" href="{{ .Paginator.Prev.URL | absURL }}">
{{ end }}
{{ if .Paginator.HasNext }}
<link rel="next" href="{{ .Paginator.Next.URL | absURL }}">
{{ end }}
<!-- Canonical URL for all pagination pages -->
<link rel="canonical" href="{{ .Paginator.URL | absURL }}">
{{ end }}
<!-- pagination.jsonld — Structured data for pagination -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ItemList",
"itemListElement": [
{{ range $index, $post := .Posts }}
{
"@type": "ListItem",
"position": {{ add $index 1 }},
"url": "{{ $post.Permalink }}"
}{{ if not (last $index $.Posts) }},{{ end }}
{{ end }}
],
"numberOfItems": {{ .TotalPosts }},
"itemListOrder": "Descending"
}
</script>
Common Mistakes
- Not handling page 1 as the root URL.
/blog/should show page 1, not/blog/page/1/. Use canonical to avoid duplicate content. - Missing noindex for pagination pages. Search engines may index pagination pages as thin content. Use noindex for page 2+ if content is minimal.
- Broken navigation when pages are 0 or negative. Validate the page parameter. Return 404 for non-existent pages.
- Not using rel=prev/rel=next. These tell search engines paginated pages are part of a series, preventing duplicate content issues.
- Generating too many pagination pages. 500 pages of 10 each = 50 pages. That's fine. But if each page has 1 result, increase the page size.
Practice Questions
- How does pagination differ between Next.js, Eleventy, and Hugo?
- What SEO considerations apply to paginated pages?
- How do you calculate total pages from a content collection size?
- What is the role of rel=prev and rel=next in pagination SEO?
- How do you handle edge cases like empty pages or invalid page numbers?
Challenge: Build a paginated blog with 50 posts: implement pagination (10 per page) with numbered page navigation, add previous/next links, set up proper SEO tags (rel prev/next, canonical), and ensure page 1 is at /blog/ not /blog/page/1/.
FAQ
Mini Project
Create a paginated documentation site with 100+ pages: group content into sections with 10 items per page, implement numbered pagination with URL structure /docs/section/page/N, add previous/next navigation, configure rel=prev/rel=next for SEO, and test with Google Search Console.
What's Next
Content is organized with pagination. Now explore SSG Internationalization to build multilingual static sites that serve global audiences.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro