Pagination SEO in Nuxt
- Each paginated page needs its own self-referencing canonical; never point every page's canonical at page 1
- Google retired
rel=prev/nextas an indexing signal in 2019, so crawlable<a href>links are what get pages 2+ discovered now - Infinite scroll needs a hybrid approach: real content for users, paginated URLs with unique canonicals for crawlers
Pagination splits content across multiple pages. The content and database snippets require your own collection schema, database client, and total count. Those data-loading snippets cover initial page setup; add route-change handling if the component remains mounted. Each page needs a distinct URL and appropriate canonical. Correct setup helps discovery but does not guarantee indexing or rankings.
Google no longer uses rel=prev/next tags as an indexing signal; it retired them in 2019. Modern pagination relies on self-referencing canonicals and crawlable <a href> links instead.
Self-Referencing Canonical Tags
Each paginated page should have its own canonical URL pointing to itself.
Don't point distinct pages to page 1. This requests consolidation of different content. Google may ignore that preference.
<script setup lang="ts">
const route = useRoute()
const page = computed(() => Number(route.query.page) || 1)
useHead(() => ({
link: [
{
rel: 'canonical',
href: `https://nuxtseo.com/blog?page=${page.value}`
}
]
}))
</script><script setup lang="ts">
// Avoid this preference for distinct pages; Google may ignore it
useHead({
link: [
{
rel: 'canonical',
href: 'https://nuxtseo.com/blog' // Always page 1
}
]
})
</script>Google's own guidance is explicit: don't use the first page of a sequence as the canonical for every page; give each page its own canonical URL instead.
Pagination URL Structure
Use query parameters or path segments. Both work for SEO.
| URL Pattern | Example | SEO Impact |
|---|---|---|
| Query parameter | /blog?page=2 | Good - simple, flexible |
| Path segment | /blog/page/2 | Good - cleaner URLs |
| Hash fragment | /blog#page=2 | Bad - Google ignores # |
Query parameter approach:
Nuxt handles query parameters automatically through file-based routing:
<script setup lang="ts">
const route = useRoute()
const page = computed(() => Number(route.query.page) || 1)
// Fetch paginated content
const { data: posts } = await useAsyncData(
'posts',
() => queryCollection('blog')
.skip((page.value - 1) * 10)
.limit(10)
.all(),
{ watch: [page] }
)
</script>Path segment approach:
Create nested route structure for cleaner URLs:
<script setup lang="ts">
// Handles /blog
const { data: posts } = await useAsyncData(
'posts',
() => queryCollection('blog').limit(10).all()
)
</script><script setup lang="ts">
// Handles /blog/page/2
const route = useRoute()
const page = Number(route.params.page)
const { data: posts } = await useAsyncData(
'posts',
() => queryCollection('blog')
.skip((page - 1) * 10)
.limit(10)
.all()
)
</script>Never use fragment identifiers (#page=2). Google ignores everything after # and may not follow a "next page" link that only differs by its fragment, since it looks identical to the page already crawled.
Crawlable Links
Google needs <a href> tags to discover paginated pages. Nuxt's SSR renders navigation in the initial HTML.
<script setup lang="ts">
const route = useRoute()
const currentPage = computed(() => Number(route.query.page) || 1)
const totalPages = 10
</script>
<template>
<nav>
<!-- Crawlable links with href -->
<NuxtLink
v-for="n in totalPages"
:key="n"
:to="{ query: { page: n } }"
:class="{ active: n === currentPage }"
>
{{ n }}
</NuxtLink>
</nav>
</template>NuxtLink generates proper <a href> tags while providing client-side navigation for users.
Pagination Component
Full example with prev/next links and numbered pages:
<script setup lang="ts">
const route = useRoute()
const currentPage = computed(() => Number(route.query.page) || 1)
const totalPages = 10
const prevPage = computed(() =>
currentPage.value > 1 ? currentPage.value - 1 : null
)
const nextPage = computed(() =>
currentPage.value < totalPages ? currentPage.value + 1 : null
)
</script>
<template>
<nav aria-label="Pagination">
<!-- Previous link -->
<NuxtLink
v-if="prevPage"
:to="{ query: { page: prevPage } }"
>
Previous
</NuxtLink>
<!-- Page numbers -->
<NuxtLink
v-for="n in totalPages"
:key="n"
:to="{ query: { page: n } }"
:aria-current="n === currentPage ? 'page' : undefined"
>
{{ n }}
</NuxtLink>
<!-- Next link -->
<NuxtLink
v-if="nextPage"
:to="{ query: { page: nextPage } }"
>
Next
</NuxtLink>
</nav>
</template>Include links from each page to following pages using <a href> tags. Googlebot follows these to discover your content.
View All Page Approach
A View All page can give readers another way to browse the collection. Keep distinct paginated pages self-canonical. A View All page's existence alone does not make each subset an equivalent duplicate. Consolidate only equivalent content; do not use noindex to choose a canonical.
Infinite Scroll vs Pagination
| Pattern | SEO Impact | UX | When to Use |
|---|---|---|---|
| Pagination | Good - all pages indexable | Predictable | Catalogs, search results |
| Infinite scroll | Poor - requires special handling | Frictionless | Social feeds, inspiration |
| Load More button | Poor - unless URL changes | Balanced | Product listings |
Infinite scroll SEO challenges:
Google Search doesn't scroll or click to load content, so anything that only appears after a scroll event stays hidden from it. Fix it with a hybrid approach: run infinite scroll for users, and update the URL as they scroll so crawlers have a paginated series to follow. Give each increment a unique, persistent URL with an absolute page number (?page=12, not something relative like ?date=yesterday), and link sequentially between pages.
Use an app-owned loading function for the button. Handle absent or failed API responses before appending records. The URL must also load the same page directly; changing browser history alone does not make new content crawlable.
Load More button:
Buttons work for readers, but Googlebot cannot click them. Use the hybrid approach above for crawlable pagination.
Server-Side Pagination
Supply your database client and total record count. These are application dependencies, not Nuxt globals.
Nuxt Content provides built-in pagination support:
<script setup lang="ts">
const route = useRoute()
const page = computed(() => Number(route.query.page) || 1)
const limit = 10
const { data: posts } = await useAsyncData(
'posts',
() => queryCollection('blog')
.skip((page.value - 1) * limit)
.limit(limit)
.all(),
{ watch: [page] }
)
const { data: totalPosts } = await useAsyncData(
'totalPosts',
() => queryCollection('blog').count()
)
const totalPages = computed(() => Math.ceil(totalPosts.value / limit))
</script>For API-based pagination:
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = 10
const offset = (page - 1) * limit
const posts = await db.query(
'SELECT * FROM posts LIMIT ? OFFSET ?',
[limit, offset]
)
return {
posts,
page,
totalPages: Math.ceil(totalPosts / limit)
}
})Limit queries to avoid performance issues. Use database indexes on sort columns.
Meta Titles and Descriptions
Differentiate each paginated page for SEO:
<script setup lang="ts">
const route = useRoute()
const page = computed(() => Number(route.query.page) || 1)
useHead(() => ({
title: page.value === 1
? 'Blog Posts'
: `Blog Posts - Page ${page.value}`,
meta: [
{
name: 'description',
content: page.value === 1
? 'Read our latest blog posts about Nuxt SEO.'
: `Blog posts page ${page.value}. Read more about Nuxt SEO.`
}
],
link: [
{
rel: 'canonical',
href: `https://nuxtseo.com/blog${page.value > 1 ? `?page=${page.value}` : ''}`
}
]
}))
</script>You may add the page number for clarity. Google allows the same title and description across a pagination sequence.
Noindex on Paginated Pages
Keep useful, distinct paginated pages indexable. If your policy excludes a selected page, use noindex and allow crawling so Google can read it. A View All page alone does not justify excluding the pagination sequence.
Common Mistakes
Mistake 1: Canonicalizing all pages to page 1
<!-- ❌ Don't do this -->
<script setup lang="ts">
useHead({
link: [
{ rel: 'canonical', href: 'https://nuxtseo.com/blog' }
]
})
</script>This requests consolidation of pages 2+. Use self-referencing canonicals for distinct content.
Mistake 2: Using hash fragments
❌ /blog#page=2
✅ /blog?page=2
✅ /blog/page/2Google ignores #. Your pagination won't be indexed.
Mistake 3: Client-only pagination links
<!-- ❌ Not crawlable -->
<button @click="nextPage">
Next
</button>
<!-- ✅ Crawlable -->
<NuxtLink :to="{ query: { page: page + 1 } }">
Next
</NuxtLink>Mistake 4: Inconsistent trailing slashes
/blog?page=1
/blog/?page=2 ← Duplicate contentPick one format. See trailing slashes guide.
Mistake 5: Blocking pagination in robots.txt
# ❌ Hides paginated content
User-agent: *
Disallow: /*?page=This wildcard prevents Google from fetching pages 2+ and reading their canonical tags. A linked blocked URL can still appear without its content. Google's own guidance reserves robots.txt for filter and sort URL variants (?sort=price, ?color=red), not the primary page sequence.
Testing Pagination SEO
1. Check canonical tags
curl -s https://nuxtseo.com/blog?page=2 | grep canonicalShould return:
<link rel="canonical" href="https://nuxtseo.com/blog?page=2">2. Verify crawlable links
View page source (not DevTools). Look for <a href> tags with pagination URLs. Nuxt's SSR ensures these are present in the initial HTML.
3. Google Search Console
- URL Inspection tool
- Check "Coverage" report for indexed pages
- Look for paginated URLs in index
4. Site search
site:nuxtseo.com/blog inurl:pageShows indexed paginated pages.
Checklist
- Each paginated page has its own self-referencing canonical, not one pointing to page 1
- Pagination uses real
<a href>links, not click handlers alone - URLs use query params or path segments, never a
#fragment - Titles and descriptions accurately describe the sequence
- Infinite scroll and Load More expose a crawlable, paginated URL alongside the interactive UI
- robots.txt doesn't block the pagination pattern
- Keep useful pagination pages indexable; choose exclusions by content, not the presence of View All
Pre-launch setup, post-launch verification, and ongoing monitoring. Interactive checklist with links to every guide.