---
title: "Duplicate Content SEO in Vue"
description: "Duplicate URLs split your ranking signals and burn crawl budget. Fix them in Vue with canonical tags, 301 redirects, and parameter handling."
canonical_url: "https://nuxtseo.com/learn-seo/vue/controlling-crawlers/duplicate-content"
last_updated: "2026-07-16"
---

<key-takeaways>

- Vue SPAs carry extra duplicate content risk from hash vs history mode routing, client-rendered pages that all return the same HTML shell, and missing server-side canonicals
- Duplicate content splits ranking signals and wastes crawl budget: Google picks a single version to index, and it's often not the one you want
- Use `@unhead/vue` to set canonical tags or 301 redirects to consolidate duplicates; query parameters are the most common source

</key-takeaways>

Vue SPAs face additional duplicate content risks beyond the usual URL variations. Vue Router's history mode vs hash mode, client-rendered pages that return identical HTML shells, and missing server-side canonical tags all compound the problem: the same content ends up living at more than one URL, which splits ranking signals and wastes crawl budget.

[Google doesn't penalize duplicate content](https://developers.google.com/search/docs/crawling-indexing/canonicalization) unless you're deliberately scraping other sites. But it hurts SEO by diluting link equity across multiple URLs and confusing search engines about which page to rank.

## Common Causes

### URL Variations

**www vs non-www**

`www.mysite.com` and `mysite.com` are [treated as separate sites](https://yoast.com/video/ask-yoast-use-www-or-not/). Choose one, redirect the other.

**HTTP vs HTTPS**

`http://mysite.com` and `https://mysite.com` create duplicates. Always redirect HTTP to HTTPS.

**Trailing slashes**

`/products` and `/products/` are different URLs. [Pick one format site-wide](/learn-seo/vue/routes-and-rendering/trailing-slashes).

<code-group>

```ts [Express - www redirect]
import express from 'express'

const app = express()

app.use((req, res, next) => {
  const host = req.get('host')

  // Force non-www
  if (host.startsWith('www.')) {
    return res.redirect(301, `https://${host.slice(4)}${req.path}`)
  }

  next()
})
```

```ts [Express - HTTPS redirect]
import express from 'express'

const app = express()

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

  next()
})
```

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

export default defineEventHandler((event) => {
  const host = getRequestHost(event)

  if (host.startsWith('www.')) {
    return sendRedirect(event, `https://${host.slice(4)}${event.path}`, 301)
  }
})
```

</code-group>

### Query Parameters

[URL parameters create exponential duplicates](/learn-seo/vue/routes-and-rendering/query-parameters). Three filters generate 8 combinations. Add sorting and pagination, and you're looking at hundreds of URLs.

```text
/products
/products?color=red
/products?color=red&size=large
/products?color=red&size=large&sort=price
/products?color=red&size=large&sort=price&page=2
```

**Fix:** Set [canonical tags](/learn-seo/vue/controlling-crawlers/canonical-urls#filter-and-sort-parameters) to point filter and sort variations to the base URL. Alternatively, [block filtered pages from indexing](/learn-seo/vue/controlling-crawlers/meta-tags) with noindex.

Parameter order matters too: `?sort=price&filter=red` and `?filter=red&sort=price` are identical content, different URLs. Enforce consistent parameter order in canonical URLs; see the [Query Parameters guide](/learn-seo/vue/routes-and-rendering/query-parameters) for the implementation.

### Tracking Parameters and Pagination

Analytics params (`utm_source`, `fbclid`, `gclid`) don't change content but create duplicate URLs, and so do un-canonicalized paginated pages.

**Fix: strip tracking parameters.** Strip tracking parameters (utm_source, fbclid, gclid) from canonical URLs. See the [Query Parameters guide](/learn-seo/vue/routes-and-rendering/query-parameters) for the composable implementation. Better yet, [redirect tracking params at the server level](/learn-seo/vue/routes-and-rendering/query-parameters#server-side-parameter-handling) for proper 301 status codes.

**Fix: self-reference paginated pages.** [Each paginated page has unique content](/learn-seo/vue/routes-and-rendering/pagination). Use self-referencing canonicals; don't point page 2 to page 1. See the [Pagination SEO guide](/learn-seo/vue/routes-and-rendering/pagination) for the implementation.

### Print and Mobile Versions

Printer-friendly URLs (`/article?print=true`) and mobile subdomains (`m.mysite.com`) create duplicates.

**Fix: Canonical to desktop version**

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

useHead({
  link: [{
    rel: 'canonical',
    // Always point to main URL
    href: 'https://mysite.com/article'
  }]
})
</script>
```

For print, use CSS `@media print` instead of separate URLs.

### Session IDs and Click Tracking

Session IDs in URLs create infinite variations.

```text
/products?sessionid=abc123
/products?sessionid=xyz789
/products?sessionid=def456
```

**Fix: Don't put session IDs in URLs.** Use cookies. If unavoidable, [block with robots.txt](/learn-seo/vue/controlling-crawlers/robots-txt):

```robots-txt [public/robots.txt]
User-agent: *
Disallow: /*?sessionid=
Disallow: /*&sessionid=
Disallow: /*?sid=
Disallow: /*&sid=
```

## Finding Duplicate Content

### Google Search Console

[Use the Page indexing report](https://support.google.com/webmasters/answer/7440203) to identify duplicates:

1. Open Search Console
2. Go to "Indexing" → "Pages"
3. Look for:

  - "Duplicate, Google chose different canonical than user"
  - "Duplicate without user-selected canonical"
  - "Alternate page with proper canonical tag"

Click each category to see affected URLs. If Google chose a different canonical than you specified, [conflicting signals exist](https://developers.google.com/search/blog/2019/03/how-to-discover-suggest-google-selected).

**Using URL Inspection:**

1. Enter any URL
2. Check "User-declared canonical" vs "Google-selected canonical"
3. If they differ, Google found stronger signals pointing to a different URL

**View page source (not DevTools)** to verify canonical tags are server-rendered:

```bash
curl https://mysite.com/products?sort=price | grep canonical
```

**Check for canonicalization conflicts:**

- Multiple `rel="canonical"` tags on same page
- Canonical in `<head>` vs HTTP header
- Canonical points to redirect or noindexed page
- Canonical URL returns 4xx/5xx status

**Test redirect chains:**

```bash
curl -I https://mysite.com/old-url
```

Should show one 301 redirect, not a chain.

### Screaming Frog

[Screaming Frog detects exact and near-duplicate content](https://www.screamingfrog.co.uk/seo-spider/tutorials/how-to-check-for-duplicate-content/):

**Exact duplicates**: Pages with identical HTML (MD5 hash match)

**Near duplicates**: Pages with 90%+ similarity (minhash algorithm)

**Setup:**

1. Enable near duplicates: `Config > Content > Duplicates`
2. Crawl your site
3. Go to "Content" tab
4. Filter by "Exact Duplicates" or "Near Duplicates"

Check these columns:

- `Closest Similarity Match`: Percentage match to most similar page
- `No. Near Duplicates`: Count of similar pages
- `Hash`: MD5 hash for exact duplicate detection

[Screaming Frog](https://screamingfrog.co.uk) auto-excludes nav and footer elements to focus on main content. [Adjust threshold](https://www.screamingfrog.co.uk/seo-spider/user-guide/tabs/#content) if needed (default 90%).

### Manual and Third-Party Tools

Use Google site search to find duplicates manually:

```text
site:mysite.com "exact title text"
```

If multiple URLs appear with the same title, you have duplicates.

**Siteliner** is a free tool that crawls up to 250 pages and shows a duplicate content percentage. **Copyscape** detects external duplicate content, i.e. other sites copying you. Both are useful for content audits but don't replace Search Console or Screaming Frog for technical SEO.

## Canonical vs 301 Redirect

<table>
<thead>
  <tr>
    <th>
      When to Use
    </th>
    
    <th>
      Canonical Tag
    </th>
    
    <th>
      301 Redirect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Need both URLs live
      </strong>
    </td>
    
    <td>
      ✅ Yes
    </td>
    
    <td>
      ❌ No
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        User should see one URL
      </strong>
    </td>
    
    <td>
      ❌ No
    </td>
    
    <td>
      ✅ Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Products in multiple categories
      </strong>
    </td>
    
    <td>
      ✅ Yes
    </td>
    
    <td>
      ❌ No
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Old page no longer needed
      </strong>
    </td>
    
    <td>
      ❌ No
    </td>
    
    <td>
      ✅ Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        UTM tracking parameters
      </strong>
    </td>
    
    <td>
      ✅ Yes
    </td>
    
    <td>
      ❌ No
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        www vs non-www
      </strong>
    </td>
    
    <td>
      ❌ No
    </td>
    
    <td>
      ✅ Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        HTTP vs HTTPS
      </strong>
    </td>
    
    <td>
      ❌ No
    </td>
    
    <td>
      ✅ Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Moved/renamed pages
      </strong>
    </td>
    
    <td>
      ❌ No
    </td>
    
    <td>
      ✅ Yes
    </td>
  </tr>
</tbody>
</table>

**Canonical tags** are [hints, not directives](https://www.searchenginejournal.com/canonical-vs-301-redirect/383124/). Google may ignore them. Both versions remain accessible. Use for duplicates you need (tracking params, multiple category paths).

**301 redirects** are permanent. Users see the redirect target. [Pass the same link equity as canonicals](/learn-seo/vue/controlling-crawlers/redirects) but remove the duplicate from the index. Use for outdated or unnecessary URLs.

**Don't combine:** Using both canonical tag and 301 redirect on the same page sends conflicting signals. Pick one.

## Decision Tree

![Duplicate Content Decision Tree](/images/learn-seo/vue/duplicate-content-decision.svg)

**Examples:**

- `http://mysite.com` → `https://mysite.com`: **301 redirect**
- `www.mysite.com` → `mysite.com`: **301 redirect**
- `/products?utm_source=twitter` → `/products`: **Canonical tag**
- `/products/shoes` and `/sale/shoes` (same product): **Canonical tag** (one canonical, one alternate)
- `/products?filter=red`: **Noindex + canonical to base URL**
- `/old-page` → `/new-page`: **301 redirect**

## Common Mistakes

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

```vue
<!-- ❌ Wrong - hides pages 2+ from search -->
<script setup lang="ts">
import { useHead } from '@unhead/vue'

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

[Each paginated page should reference itself](/learn-seo/vue/routes-and-rendering/pagination#self-referencing-canonical-tags).

**Mistake 2: Using relative canonical URLs**

<code-group>

```html [❌ Bad]
<!-- Must be absolute -->
<link rel="canonical" href="/products/phone">
```

```html [✅ Good]
<link rel="canonical" href="https://mysite.com/products/phone">
```

</code-group>

[Google requires absolute URLs](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls).

**Mistake 3: Combining canonical with noindex**

<code-group>

```vue [❌ Bad]
<script setup lang="ts">
import { useHead, useSeoMeta } from '@unhead/vue'

// Conflicting signals - pick one approach
useHead({
  link: [{ rel: 'canonical', href: 'https://mysite.com/page' }]
})
useSeoMeta({
  robots: 'noindex, follow'
})
</script>
```

```vue [✅ Good - Canonical Only]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

// Use canonical when page should be indexed but consolidated
useHead({
  link: [{ rel: 'canonical', href: 'https://mysite.com/page' }]
})
</script>
```

```vue [✅ Good - Noindex Only]
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

// Use noindex when page should not appear in search
useSeoMeta({
  robots: 'noindex, follow'
})
</script>
```

</code-group>

Canonical says "this is a duplicate of X." Noindex says "don't index this." [Pick one](/learn-seo/vue/controlling-crawlers/canonical-urls).

**Mistake 4: Canonical chains**

```text
Page A → canonical → Page B → canonical → Page C
```

A chain dilutes the signal and search engines may not follow it all the way through. Canonical directly to the final target instead.

**Mistake 5: Client-side canonicals in SPAs**

Googlebot doesn't execute JavaScript fast enough. [Server-render canonical tags](/learn-seo/vue/routes-and-rendering/rendering) or use SSR.

## Preventing Duplicate Content

### Configure Vue Router

Use consistent trailing slash handling:

```ts [router/index.ts]
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [/* ... */],
  strict: true // Treat /page and /page/ as different
})

// Enforce trailing slashes
router.beforeEach((to, from, next) => {
  if (!to.path.endsWith('/') && to.path !== '/') {
    next({ path: `${to.path}/`, query: to.query })
  }
  else {
    next()
  }
})
```

Or [redirect at the server level](/learn-seo/vue/routes-and-rendering/trailing-slashes) for proper 301 status codes.

### Validate Parameter Values

Prevent infinite URL variations by whitelisting allowed parameter values:

```ts
const allowedSortValues = ['price', 'name', 'date', 'rating']
const sort = route.query.sort

if (sort && !allowedSortValues.includes(sort)) {
  // Redirect to base URL or default sort
  router.replace({ query: { ...route.query, sort: undefined } })
}
```

### Block Low-Value Pages

Use [robots.txt](/learn-seo/vue/controlling-crawlers/robots-txt) to block search results, filtered pages, and admin sections:

```robots-txt [public/robots.txt]
User-agent: *

# Block search results
Disallow: /search?
Disallow: /*?q=
Disallow: /*?query=

# Block filters
Disallow: /*?filter=
Disallow: /*&filter=

# Block tracking params
Disallow: /*?utm_source=
Disallow: /*?fbclid=
Disallow: /*?gclid=

# Block session IDs
Disallow: /*?sessionid=
Disallow: /*?sid=
```

Using Nuxt? [Nuxt SEO](/docs/nuxt-seo/getting-started/introduction) handles canonical URLs automatically through site config and route rules. [Learn more about duplicate content in Nuxt →](/learn-seo/nuxt/controlling-crawlers)

## Checklist

<checklist id="vue-duplicate-content">

- Choose one canonical domain (www or non-www, HTTP or HTTPS) and redirect the rest
- Pick one trailing slash format in Vue Router and enforce it site-wide
- Set canonical tags on filtered, sorted, and paginated URLs pointing to the preferred version
- Strip tracking parameters (utm_source, fbclid, gclid) from canonical URLs
- Use absolute URLs in every canonical tag, never relative paths
- Never combine noindex with a canonical tag on the same page

</checklist>
