---
title: "Pagination SEO in Nuxt"
description: "How to implement SEO-friendly pagination in Nuxt using canonical tags, self-referencing URLs, and proper link structure."
canonical_url: "https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/pagination"
last_updated: "2026-05-06T18:48:42.141Z"
---

<key-takeaways>

- Each paginated page needs its own self-referencing canonical; don't point all to page 1
- Google deprecated `rel=prev/next` in March 2019. use crawlable `<a href>` links instead
- Infinite scroll requires hybrid approach with hidden pagination links for crawlers

</key-takeaways>

Pagination 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](https://developers.google.com/search/blog/2011/09/pagination-with-relnext-and-relprev) `rel=prev/next` tags (deprecated March 2019). Modern pagination relies on self-referencing canonicals and crawlable `<a href>` links.

## Self-Referencing Canonical Tags

Each paginated page should have its own canonical URL pointing to itself.

![Pagination Canonical Flow](/images/learn-seo/vue/pagination-canonical-flow.svg)

**Don't** point all pages to page 1. That tells Google only page 1 matters, hiding pages 2+ from search results.

<code-group>

```vue [✅ Correct]
<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>
```

```vue [❌ Wrong]
<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>
```

</code-group>

[Self-referencing canonicals](https://www.semrush.com/blog/pagination-seo/) tell Google each page has unique content worth indexing.

## Pagination URL Structure

Use query parameters or path segments. Both work for SEO.

<table>
<thead>
  <tr>
    <th>
      URL Pattern
    </th>
    
    <th>
      Example
    </th>
    
    <th>
      SEO Impact
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Query parameter
    </td>
    
    <td>
      <code>
        /blog?page=2
      </code>
    </td>
    
    <td>
      Good - simple, flexible
    </td>
  </tr>
  
  <tr>
    <td>
      Path segment
    </td>
    
    <td>
      <code>
        /blog/page/2
      </code>
    </td>
    
    <td>
      Good - cleaner URLs
    </td>
  </tr>
  
  <tr>
    <td>
      Hash fragment
    </td>
    
    <td>
      <code>
        /blog#page=2
      </code>
    </td>
    
    <td>
      Bad - Google ignores <code>
        #
      </code>
    </td>
  </tr>
</tbody>
</table>

**Query parameter approach:**

Nuxt handles query parameters automatically through file-based routing:

```vue [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',
  () => queryContent('/blog')
    .skip((page.value - 1) * 10)
    .limit(10)
    .find(),
  { watch: [page] }
)
</script>
```

**Path segment approach:**

Create nested route structure for cleaner URLs:

```vue [pages/blog/index.vue]
<script setup lang="ts">
// Handles /blog
const { data: posts } = await useAsyncData(
  'posts',
  () => queryContent('/blog').limit(10).find()
)
</script>
```

```vue [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',
  () => queryContent('/blog')
    .skip((page - 1) * 10)
    .limit(10)
    .find()
)
</script>
```

Never use fragment identifiers (`#page=2`). [Google ignores everything after `#`](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading).

## Crawlable Links

Google needs `<a href>` tags to discover paginated pages. Nuxt's SSR renders navigation in the initial HTML.

```vue
<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:

```vue
<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](https://www.amsive.com/insights/seo/how-to-correctly-implement-pagination-for-seo-user-experience/) using `<a href>` tags. Googlebot follows these to discover your content.

## View All Page Approach

Offer a single page with all content. Point canonicals from paginated pages to the View All page.

```vue
<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:**

- Slow page load with 100+ items
- Poor mobile experience
- Images kill performance

[View All pages work](https://seotactica.com/learn/canonical-pagination/) for small datasets (50 items). For large catalogs, use self-referencing canonicals.

## Infinite Scroll vs Pagination

![Pagination vs Infinite Scroll Decision](/images/learn-seo/vue/pagination-vs-infinite-scroll.svg)

<table>
<thead>
  <tr>
    <th>
      Pattern
    </th>
    
    <th>
      SEO Impact
    </th>
    
    <th>
      UX
    </th>
    
    <th>
      When to Use
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Pagination
    </td>
    
    <td>
      Good - all pages indexable
    </td>
    
    <td>
      Predictable
    </td>
    
    <td>
      Catalogs, search results
    </td>
  </tr>
  
  <tr>
    <td>
      Infinite scroll
    </td>
    
    <td>
      Poor - requires special handling
    </td>
    
    <td>
      Frictionless
    </td>
    
    <td>
      Social feeds, inspiration
    </td>
  </tr>
  
  <tr>
    <td>
      Load More button
    </td>
    
    <td>
      Poor - unless URL changes
    </td>
    
    <td>
      Balanced
    </td>
    
    <td>
      Product listings
    </td>
  </tr>
</tbody>
</table>

**Infinite scroll SEO challenges:**

[Googlebot cannot scroll](https://www.seoclarity.net/blog/pagination-vs-infinite-scroll). Content below the fold stays hidden. Solutions:

1. **Hybrid approach** - Infinite scroll for users, paginated URLs for crawlers:

```vue
<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
  await navigateTo({ query: { page: page.value } }, { replace: true })
}

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

1. **Component pages** - Break infinite scroll into paginated URLs with unique canonicals. [Google recommends paginated series](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading) alongside infinite scroll.

**Load More button:**

Works for UX but [Googlebot can't click buttons](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading). Use the hybrid approach above if SEO matters.

## Server-Side Pagination

Nuxt Content provides built-in pagination support:

```vue [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',
  () => 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:

```ts [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:

```vue
<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](/learn-seo/nuxt/controlling-crawlers/duplicate-content) confusion ([source](https://www.searchenginejournal.com/technical-seo/pagination/)).

## Noindex on Paginated Pages

**Don't** noindex paginated pages. [This loses indexed content](https://www.seoclarity.net/blog/pagination-seo). Google stops crawling noindexed pages, hiding your products/articles.

Only noindex if:

- Filter/sort variations create infinite URLs (`/blog?sort=date&order=asc&filter=nuxt`)
- You have a View All page as canonical
- Pages have no unique content

For most sites, [keep paginated pages indexable](https://ahrefs.com/blog/rel-prev-next-pagination/).

## Common Mistakes

**Mistake 1: Canonicalizing all pages to page 1**

```vue
<!-- ❌ 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**

```text
❌ /blog#page=2
✅ /blog?page=2
✅ /blog/page/2
```

Google ignores `#`. Your pagination won't be indexed.

**Mistake 3: Client-only pagination links**

```vue
<!-- ❌ Not crawlable -->
<button @click="nextPage">
Next
</button>

<!-- ✅ Crawlable -->
<NuxtLink :to="{ query: { page: page + 1 } }">
Next
</NuxtLink>
```

**Mistake 4: Inconsistent trailing slashes**

```text
/blog?page=1
/blog/?page=2  ← Duplicate content
```

Pick one format. See [trailing slashes guide](/learn-seo/nuxt/routes-and-rendering/trailing-slashes).

**Mistake 5: Blocking pagination in robots.txt**

```robots-txt
# ❌ Hides paginated content
User-agent: *
Disallow: /*?page=
```

[Don't block pagination URLs](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading). Google needs to crawl them.

## Testing Pagination SEO

**1. Check canonical tags**

```bash
curl -s https://nuxtseo.com/blog?page=2 | grep canonical
```

Should return:

```html
<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**

```text
site:nuxtseo.com/blog inurl:page
```

Shows indexed paginated pages.
