Index of every page on this site: llms.txt

Pagination SEO in Vue

Give each Vue pagination page a stable URL, reactive canonical, and crawlable links. Verify initial content and handle missing pages separately.
Harlan Wilton14 mins read Published Updated
What you'll learn
  • Give each page in a sequence its own URL and self-referencing canonical; do not point every canonical to page one
  • Google no longer uses rel=prev/next to identify pagination. Link sequentially with real anchor hrefs
  • Infinite scroll needs persistent chunk URLs and crawlable links. Changing the address after a click does not make hidden content discoverable
  • Canonicals and links support discovery and canonical selection; they do not guarantee indexing or rankings

Pagination divides a collection into pages. Google's pagination guidance recommends unique URLs, self-referencing canonicals, and links to following pages. It also permits the same title and description across a paginated sequence.

Self-Referencing Canonical Tags

For a sequence such as /blog, /blog?page=2, and /blog?page=3, each valid page should reference itself. Do not declare page one as the canonical for every page: their item sets differ.

The component below updates its canonical from a reactive page value. It uses /blog as the preferred first-page URL. Missing or invalid pages get no canonical in this example. Those response statuses still require server or host handling.

Pagination URL Structure

Both /blog?page=2 and /blog/page/2 can identify a page. Choose one pattern and make direct requests serve the same item set users see through navigation. Do not use #page=2 to identify a separate page: Google generally ignores fragments for this purpose.

For the query-based example below, declare a /blog route that renders the component. Install Vue Router and Unhead in the app. Use the Vite SSR setup or prerendering setup when these tags and links must appear in initial HTML. Static generation must include each intended page URL.

RouterLink renders an anchor with an href by default. Google can discover those URLs in successfully rendered output. SSR or prerendering exposes them in the initial response and reduces dependence on successful JavaScript rendering; a client-only app is not automatically undiscoverable.

A button or click handler alone is insufficient for link discovery. The component below supplies numbered, previous, and next links through ordinary RouterLink components.

Pagination Component

This complete component uses twenty illustrative posts, two per page. It demonstrates route query parsing, content selection, canonical output, and navigation. It does not implement a CMS or API loader. In a real collection, derive the count and stable item ordering from your data source.

Place both files beside each other and render Pagination.vue at your installed router's /blog route:

src/pages/pagination-page.ts
import type { LocationQueryValue } from 'vue-router'

export function parsePage(raw: LocationQueryValue | LocationQueryValue[] | undefined, totalPages: number): number | null {
  if (raw === undefined)
    return 1
  if (typeof raw !== 'string' || !/^[1-9]\d*$/.test(raw))
    return null
  const page = Number(raw)
  return Number.isSafeInteger(page) && page <= totalPages ? page : null
}
src/pages/Pagination.vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { computed } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import { parsePage } from './pagination-page'

const route = useRoute()
// Illustrative data; a real app loads a stable page and its count through its data layer.
const posts = Array.from({ length: 20 }, (_, index) => ({ id: index + 1, title: `Post ${index + 1}` }))
const perPage = 2
const totalPages = Math.ceil(posts.length / perPage)
const page = computed(() => parsePage(route.query.page, totalPages))
const visiblePosts = computed(() => page.value === null ? [] : posts.slice((page.value - 1) * perPage, page.value * perPage))
const prevPage = computed(() => page.value !== null && page.value > 1 ? page.value - 1 : null)
const nextPage = computed(() => page.value !== null && page.value < totalPages ? page.value + 1 : null)
const pageUrl = (value: number) => value === 1 ? '/blog' : `/blog?page=${value}`

useHead(() => ({
  title: page.value === null ? 'Page Not Found' : `Blog Posts, Page ${page.value}`,
  meta: [{ name: 'robots', content: page.value === null ? 'noindex' : 'index, follow' }],
  link: page.value === null ? [] : [{ rel: 'canonical', href: `https://example.com${pageUrl(page.value)}` }]
}))
</script>

<template>
  <main v-if="page !== null">
    <h1>Blog Posts, Page {{ page }}</h1>
    <article v-for="post in visiblePosts" :key="post.id">
      {{ post.title }}
    </article>
    <nav aria-label="Pagination">
      <RouterLink v-if="prevPage !== null" :to="pageUrl(prevPage)" aria-current-value="false">
        Previous
      </RouterLink>
      <RouterLink v-for="n in totalPages" :key="n" :to="pageUrl(n)" :aria-current-value="n === page ? 'page' : 'false'">
        {{ n }}
      </RouterLink>
      <RouterLink v-if="nextPage !== null" :to="pageUrl(nextPage)" aria-current-value="false">
        Next
      </RouterLink>
    </nav>
  </main>
  <main v-else>
    <h1>Page Not Found</h1>
  </main>
</template>

Vue Router's active-link test compares matched routes and params without considering query values. The explicit aria-current-value settings mark only the matching page number as current, not Previous or Next.

The parser rejects negative, fractional, repeated, malformed, and out-of-range page values. The first page omits the query in its canonical. This particular blog example has no content-changing filter or sort parameters. If yours does, preserve their meaning in links and canonicals.

The invalid state shows a missing-page message and noindex. It cannot change an already-sent HTTP status. A request-time server should return an appropriate 404 for nonexistent pages; a client-only app needs a deliberate missing-page policy.

View All Page Approach

A View All page can be an optional navigation choice. It does not automatically replace the canonical of every subset page. Google's pagination guidance recommends a canonical for each page in the sequence.

Measure the actual payload, DOM, images, and interactions before replacing pagination with one large page. A hundred items is not a universal performance threshold.

Infinite Scroll vs Pagination

PatternDiscovery requirement
PaginationStable page URLs linked sequentially
Infinite scrollPersistent chunk URLs and links that do not require scrolling
Load MoreContent also available at linked page URLs without clicking the button

Google does not interact with the page to load content. Give each chunk a persistent URL, keep its content consistent on direct visits, and link the sequence. Do not provide a separate, materially different collection only for bots.

Updating the displayed URL when a new chunk becomes the primary visible content helps users refresh and share that location. It does not itself load that chunk for a crawler. Verify both direct URL content and sequential links.

Server-Side Pagination

A real data layer must parse page values, use a supported page size, apply stable ordering, and calculate the range from the collection. Reject nonexistent pages deliberately. Reuse the same initial item set during hydration.

A Vite development proxy is not a production API or a server pagination implementation. Database helpers and schemas belong to your app; do not copy an undefined db.query placeholder as a complete service. Connect the component to your documented loader and test its data, error, and direct-request behavior.

Meta Titles and Descriptions

The example adds a page number to its title to help users identify their position. Google permits the same titles and descriptions across a paginated sequence. Duplicate descriptions alone do not establish a duplicate-content penalty or a canonical selection failure.

Use reactive head values when page queries change without remounting the component. Do not snapshot Number(route.query.page) once and expect it to update after navigation.

Noindex on Paginated Pages

If pages should be eligible for Search, keep them crawlable and avoid noindex. A noindexed page is excluded from Search; this does not mean Google immediately stops crawling it.

Filtered or sorted views have a separate policy based on their actual content and intended indexing. A View All page is not a general reason to noindex the primary sequence. See query parameter policies.

Common Mistakes

Pointing all canonicals to page one: the collection's later pages show different items. Give each its own canonical rather than treating them all as page-one copies.

Fragment-only pages: /blog#page=2 does not provide a separate request URL for page two. Use a path or query value.

Button-only navigation: a click handler cannot substitute for an anchor href. RouterLink supplies a link and handles in-app navigation.

Inconsistent slash policies: align links, canonicals, and served URLs. A slash difference is not proof that different page numbers contain duplicate content.

Blocking the sequence: a robots.txt rule that matches page URLs prevents fetching their content and canonical tags. It does not guarantee those URLs disappear from Search.

Testing Pagination SEO

  1. Fetch the first, middle, and last URLs directly. Check status and actual item content, not just the app shell.
  2. Inspect initial HTML and rendered DOM separately. Verify a single intended canonical and sequential anchor hrefs.
  3. Navigate between page values without reloading. Check the item set, title, canonical, and current-page indicator.
  4. Test malformed, repeated, negative, and out-of-range page queries. Verify your missing-page response and UI.
  5. Compare indexed information and rendered output in Search Console's URL Inspection. The Page indexing report replaces the older Coverage report. A site: search is not a complete inventory.

Checklist

Checklist
  • Each valid page has a stable URL and self-referencing canonical
  • Sequential navigation supplies real anchor hrefs
  • Direct visits show the same page content as in-app navigation
  • Invalid pages receive a deliberate missing-page response and UI
  • Infinite scroll or Load More content remains reachable at linked chunk URLs
  • Canonical, item, and title state updates when the page query changes

The Nuxt pagination guide applies these URL and discovery principles using Nuxt's routing and data APIs.

Was this page helpful?