Index of every page on this site: llms.txt

Pagination SEO in Nuxt

Give paginated Nuxt pages unique URLs, self-referencing canonicals, and crawlable links. Check that crawlers can reach each page.
Harlan Wilton14 mins read Published Updated
What you'll learn
  • Each paginated page needs its own self-referencing canonical; never point every page's canonical at page 1
  • Google retired rel=prev/next as 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>

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 PatternExampleSEO Impact
Query parameter/blog?page=2Good - simple, flexible
Path segment/blog/page/2Good - cleaner URLs
Hash fragment/blog#page=2Bad - Google ignores #

Query parameter approach:

Nuxt handles query parameters automatically through file-based routing:

pages/blog.vue
<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:

pages/blog/index.vue
<script setup lang="ts">
// Handles /blog
const { data: posts } = await useAsyncData(
  'posts',
  () => queryCollection('blog').limit(10).all()
)
</script>
pages/blog/page/[page].vue
<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.

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

PatternSEO ImpactUXWhen to Use
PaginationGood - all pages indexablePredictableCatalogs, search results
Infinite scrollPoor - requires special handlingFrictionlessSocial feeds, inspiration
Load More buttonPoor - unless URL changesBalancedProduct 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:

pages/blog.vue
<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:

server/api/posts.get.ts
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/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=

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 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

  • URL Inspection tool
  • Check "Coverage" report for indexed pages
  • Look for paginated URLs in index

4. Site search

site:nuxtseo.com/blog inurl:page

Shows indexed paginated pages.

Checklist

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
Was this page helpful?