rel=prev/next in March 2019—use crawlable <a href> links insteadPagination splits content across multiple pages. Search engines treat each page as separate. Set canonical tags wrong and Google indexes page 1 only. Set them right and all pages rank.
Google no longer uses rel=prev/next tags (deprecated March 2019). Modern pagination relies on self-referencing canonicals and crawlable <a href> links.
Each paginated page should have its own canonical URL pointing to itself.
Don't point all pages to page 1. That tells Google only page 1 matters, hiding pages 2+ from search results.
<script setup lang="ts">
const route = useRoute()
const page = Number(route.query.page) || 1
useHead({
link: [
{
rel: 'canonical',
href: `https://nuxtseo.com/blog?page=${page}`
}
]
})
</script>
<script setup lang="ts">
// Don't do this - hides pages 2+ from search
useHead({
link: [
{
rel: 'canonical',
href: 'https://nuxtseo.com/blog' // Always page 1
}
]
})
</script>
Self-referencing canonicals tell Google each page has unique content worth indexing.
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',
() => queryContent('/blog')
.skip((page.value - 1) * 10)
.limit(10)
.find(),
{ 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',
() => queryContent('/blog').limit(10).find()
)
</script>
<script setup lang="ts">
// Handles /blog/page/2
const route = useRoute()
const page = Number(route.params.page)
const { data: posts } = await useAsyncData(
'posts',
() => queryContent('/blog')
.skip((page - 1) * 10)
.limit(10)
.find()
)
</script>
Never use fragment identifiers (#page=2). Google ignores everything after #.
Google needs <a href> tags to discover paginated pages. Nuxt's SSR ensures navigation is rendered 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.
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.
Offer a single page with all content. Point canonicals from paginated pages to the View All page.
<script setup lang="ts">
const route = useRoute()
const showAll = route.query.show === 'all'
useHead({
link: [
{
rel: 'canonical',
href: 'https://nuxtseo.com/blog?show=all'
}
]
})
</script>
<template>
<div>
<NuxtLink to="/blog?show=all">
View All
</NuxtLink>
<!-- Paginated content -->
<article v-for="post in paginatedPosts" :key="post.id">
{{ post.title }}
</article>
</div>
</template>
Drawbacks:
View All pages work for small datasets (50 items). For large catalogs, use self-referencing canonicals.
| 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:
Googlebot cannot scroll. Content below the fold stays hidden. Solutions:
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
const posts = ref([])
const page = ref(Number(route.query.page) || 1)
async function loadMore() {
page.value++
const { data: newPosts } = await useFetch(`/api/posts?page=${page.value}`)
posts.value.push(...newPosts.value)
// Update URL for crawlers
router.replace({ query: { page: page.value } })
}
const { data: initialPosts } = await useFetch(`/api/posts?page=${page.value}`)
posts.value = initialPosts.value
</script>
<template>
<div>
<article v-for="post in posts" :key="post.id">
{{ post.title }}
</article>
<button @click="loadMore">
Load More
</button>
<!-- Crawlable pagination links -->
<nav class="sr-only">
<NuxtLink :to="{ query: { page: page + 1 } }">
Next Page
</NuxtLink>
</nav>
</div>
</template>
Load More button:
Works for UX but Googlebot can't click buttons. Use the hybrid approach above if SEO matters.
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',
() => queryContent('/blog')
.skip((page.value - 1) * limit)
.limit(limit)
.find(),
{ watch: [page] }
)
const { data: totalPosts } = await useAsyncData(
'totalPosts',
() => queryContent('/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.
Differentiate each paginated page for SEO:
<script setup lang="ts">
const route = useRoute()
const page = Number(route.query.page) || 1
useHead({
title: page === 1
? 'Blog Posts'
: `Blog Posts - Page ${page}`,
meta: [
{
name: 'description',
content: page === 1
? 'Read our latest blog posts about Nuxt SEO.'
: `Blog posts page ${page}. Read more about Nuxt SEO.`
}
],
link: [
{
rel: 'canonical',
href: `https://nuxtseo.com/blog${page > 1 ? `?page=${page}` : ''}`
}
]
})
</script>
Unique titles prevent duplicate content confusion.
Don't noindex paginated pages. This loses indexed content. Google stops crawling noindexed pages, hiding your products/articles.
Only noindex if:
/blog?sort=date&order=asc&filter=nuxt)For most sites, keep paginated pages indexable.
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 tells Google pages 2+ are duplicates. Use self-referencing canonicals.
Mistake 2: Using hash fragments
❌ /blog#page=2
✅ /blog?page=2
✅ /blog/page/2
Google 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 content
Pick one format. See trailing slashes guide.
Mistake 5: Blocking pagination in robots.txt
# ❌ Hides paginated content
User-agent: *
Disallow: /*?page=
Don't block pagination URLs. Google needs to crawl them.
1. Check canonical tags
curl -s https://nuxtseo.com/blog?page=2 | grep canonical
Should 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
4. Site search
site:nuxtseo.com/blog inurl:page
Shows indexed paginated pages.