---
title: "Canonical URLs in Vue"
description: "Add canonical URLs in Vue with @unhead/vue and Vue Router so duplicate pages don't split your rankings across builds."
canonical_url: "https://nuxtseo.com/learn-seo/vue/controlling-crawlers/canonical-urls"
last_updated: "2026-07-16"
---

<key-takeaways>

- Vue Router doesn't set canonicals automatically; use `@unhead/vue` to add them per route
- Canonical tags are hints, not directives: Google may choose a different canonical
- Always use absolute URLs (`https://mysite.com/page`, not `/page`)
- Self-referencing canonicals are recommended even for unique pages

</key-takeaways>

In Vue applications, canonical URLs require manual setup using `@unhead/vue` and Vue Router. Unlike Nuxt, which handles canonicals automatically through site config, Vue developers must explicitly set the `rel="canonical"` link tag for every route.

Use canonicals for URLs with query parameters (filters, sorting), the same content on multiple paths, paginated sequences, and cross-domain syndication. For choosing between canonicals and redirects, see [Duplicate Content](/learn-seo/vue/controlling-crawlers/duplicate-content). For redirecting users, use [HTTP redirects](/learn-seo/vue/controlling-crawlers/redirects) instead. For blocking pages from search, use [meta robots](/learn-seo/vue/controlling-crawlers/meta-tags) with noindex.

## Quick Setup

Add canonical URLs to your Vue pages using Unhead composables:

```vue [Basic Usage]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  link: [
    {
      rel: 'canonical',
      href: 'https://mysite.com/products/phone'
    }
  ]
})
</script>
```

```vue [With Query Params]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

const sort = 'price'

// Keep sort parameter in canonical
useHead({
  link: [
    {
      rel: 'canonical',
      href: `https://mysite.com/products?sort=${sort}`
    }
  ]
})
</script>
```

```vue [Cross Domain]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  link: [
    {
      rel: 'canonical',
      href: 'https://otherdomain.com/original-article'
    }
  ]
})
</script>
```

For Vue applications, you'll need to [install Unhead manually](https://unhead.unjs.io/guide/getting-started/installation).

## Understanding Canonical URLs

A canonical URL is implemented as a link tag in your page's head:

```html
<link rel="canonical" href="https://mysite.com/page">
```

### Canonical Tags Are Hints, Not Directives

Google treats canonicals as [strong signals, not mandatory rules](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls): Google does not require any canonicalization method, and a site can rank fine without specifying a preference at all. Google weighs your signal alongside other factors when it decides which URL to show in results.

Google may choose a different canonical than you specify when:

- Content differs significantly between URLs
- Multiple conflicting canonical declarations exist
- Google believes a different page is more authoritative

[Google uses the canonical page](https://developers.google.com/search/docs/crawling-indexing/canonicalization) as the main source to evaluate content and quality. Non-canonical URLs may still be crawled but usually won't appear in search results.

### Important Notes

[Self-referencing canonicals are best practice](https://searchengineland.com/canonicalization-seo-448161) even for pages with a single, unique source of content: they establish a clear preferred URL and prevent search engines from guessing when tracking parameters or alternate URL formats appear.

- Must use absolute URLs ([Google documentation](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls))
- Only one canonical per page (multiple declarations cause Google to ignore all hints)
- Best served in the initial HTML: AI crawlers like GPTBot and ClaudeBot [never execute JavaScript](https://vercel.com/blog/the-rise-of-the-ai-crawler), so a canonical injected client-side is invisible to them
- Include canonical pages in [sitemaps](/learn-seo/vue/controlling-crawlers/sitemaps), exclude non-canonical pages
- Don't combine a canonical with [noindex meta robots](/learn-seo/vue/controlling-crawlers/meta-tags): they send conflicting signals

<warning>

Canonicals only work when the target page has substantially similar content to the source. Pointing a canonical at a different page or your homepage doesn't consolidate rankings; Google is likely to ignore it.

</warning>

## Common Patterns

### Filter and Sort Parameters

```vue [pages/products/[category].vue]
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const { sort } = route.query
const category = route.params.category

useHead({
  link: [{
    rel: 'canonical',
    // Only include sort in canonical, remove filter and pagination
    href: sort
      ? `https://mysite.com/products/${category}?sort=${sort}`
      : `https://mysite.com/products/${category}`
  }]
})
</script>
```

Paginated pages need their own self-referencing canonicals too. [Don't point them all to page 1](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading): each page in the sequence has unique content and should be indexed separately. See the [Pagination SEO guide](/learn-seo/vue/routes-and-rendering/pagination) for implementation details.

### Mobile/Desktop Versions

If you serve separate mobile URLs (`m.domain.com`), [the desktop URL is always the canonical](https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing): point `rel=canonical` on the mobile page to the desktop URL, and `rel=alternate` on the desktop page to the mobile URL.

```vue [pages/products/[id].vue]
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const id = route.params.id

useHead({
  link: [{
    rel: 'canonical',
    // Mobile site (m.mysite.com) points to desktop
    href: `https://mysite.com/products/${id}`
  }]
})
</script>
```

Responsive design with a single URL avoids needing this canonical/alternate pairing at all, and is what Google recommends for new sites.

### Cross-Domain Syndication

[Google no longer recommends cross-domain canonicals](https://searchengineland.com/google-no-longer-recommends-canonical-tags-for-syndicated-content-406491) for syndicated content; a canonical tag doesn't reliably stop a syndicated copy from outranking the original. Have syndication partners block indexing with noindex meta robots instead.

```vue [pages/article/[slug].vue]
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

// If syndicating TO other sites, have them use noindex
useSeoMeta({
  robots: 'noindex, follow'
})
</script>
```

```vue [pages/original-article.vue]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

// If this IS the original, use self-referencing canonical
useHead({
  link: [{
    rel: 'canonical',
    href: 'https://mysite.com/articles/original-slug'
  }]
})
</script>
```

## Testing

Use [Google Search Console's URL Inspection tool](https://support.google.com/webmasters/answer/9012289) to compare your declared canonical with Google's selected canonical. For a full detection workflow, see [Duplicate Content](/learn-seo/vue/controlling-crawlers/duplicate-content#finding-duplicate-content).

- Validate absolute URL format
- Check for canonical chains (A → B → C)
- Verify SSR implementation (view page source, not inspected HTML)
- Test with and without parameters
- Don't combine canonical with noindex [meta robots](/learn-seo/vue/controlling-crawlers/meta-tags)

## Handling Edge Cases

### Multiple Language Versions

For multilingual sites, combine canonicals with hreflang:

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  link: [
    {
      rel: 'canonical',
      href: 'https://mysite.com/en/page'
    },
    {
      rel: 'alternate',
      hreflang: 'fr',
      href: 'https://mysite.com/fr/page'
    }
  ]
})
</script>
```

### Protocol/WWW Variations

Handle through [server redirects](/learn-seo/vue/controlling-crawlers/redirects) rather than canonicals:

<code-group>

```ts [Express]
import express from 'express'

const app = express()

app.use((req, res, next) => {
  const host = req.get('host')
  if (!host.startsWith('www.')) {
    return res.redirect(301, `https://www.${host}${req.path}`)
  }
  next()
})
```

```ts [Vite]
// server.js for Vite SSR
import express from 'express'

const app = express()

app.use((req, res, next) => {
  const host = req.get('host')
  if (!host.startsWith('www.')) {
    return res.redirect(301, `https://www.${host}${req.path}`)
  }
  next()
})
```

```ts [H3]
import { defineEventHandler, getRequestHost, sendRedirect } from 'h3'

export default defineEventHandler((event) => {
  const host = getRequestHost(event)
  if (!host.startsWith('www.')) {
    return sendRedirect(event, `https://www.${host}${event.path}`, 301)
  }
})
```

</code-group>

### Dynamic Canonicals

For dynamic routes, ensure canonical URLs are consistent:

```ts [composables/useCanonical.ts]
export function getCanonical(path: string) {
  const siteUrl = import.meta.env.VITE_SITE_URL
  return {
    link: [{
      rel: 'canonical',
      href: `${siteUrl}${path}`
    }]
  }
}
```

## Checklist

<checklist id="vue-canonical-urls">

- Every route has a self-referencing canonical, even pages with no duplicates
- Canonical URLs are absolute (`https://...`), never relative
- Only one canonical tag exists per page
- Canonicals ship in the server-rendered HTML, not injected client-side
- Paginated pages each get their own canonical, not all pointing to page 1
- Canonical and noindex are never combined on the same page

</checklist>

If you're using Nuxt, check out [Nuxt SEO](/docs/nuxt-seo/getting-started/introduction) which handles much of this automatically: [canonical URLs in Nuxt →](/learn-seo/nuxt/controlling-crawlers/canonical-urls)
