---
title: "Query Parameters and SEO in Vue"
description: "Query parameters create duplicate content and waste crawl budget. Here's how to handle filters, sorting, and tracking params in Vue Router."
canonical_url: "https://nuxtseo.com/learn-seo/vue/routes-and-rendering/query-parameters"
last_updated: "2025-12-17"
---

<key-takeaways>

- Filter parameters should be noindexed or canonical to base URL
- Strip tracking params (utm_, fbclid, gclid) from canonical URLs
- Move important filters to path segments for better SEO value

</key-takeaways>

For deciding whether content belongs in a path segment or query parameter, see [URL Structure](/learn-seo/vue/routes-and-rendering/url-structure#path-segments-vs-query-parameters).

Query parameters (`?sort=price&filter=red`) create duplicate content. In Vue, you access these through `useRoute().query` from `vue-router`, and managing their SEO impact requires manual canonical setup with `@unhead/vue`. The URL `/products?sort=price` and `/products?sort=name` show the same products in different orders, but search engines treat them as separate pages.

With filters, sorting, and pagination, a single page can generate hundreds of URL variations. Each variation wastes crawl budget and dilutes ranking signals across duplicates.

## Quick Setup

Handle query parameters with canonical URLs to tell search engines which version to index:

```ts [pages/Products.vue]
import { useHead } from '@unhead/vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const { sort, filter, page } = route.query

useHead({
  link: [{
    rel: 'canonical',
    // Remove filter and page, keep sort
    href: sort
      ? `https://mysite.com/products?sort=${sort}`
      : 'https://mysite.com/products'
  }]
})
```

```ts [pages/Products.vue - Block All Parameters]
useHead({
  link: [{
    rel: 'canonical',
    // Always canonical to base URL
    href: 'https://mysite.com/products'
  }]
})
```

```ts [pages/Products.vue - Block from Indexing]
import { useSeoMeta } from '@unhead/vue'

// Use noindex for filtered/sorted views
useSeoMeta({
  robots: computed(() =>
    route.query.filter || route.query.page
      ? 'noindex, follow'
      : 'index, follow'
  )
})
```

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

## Common Parameter Types

Different query parameters need different handling:

<table>
<thead>
  <tr>
    <th>
      Parameter Type
    </th>
    
    <th>
      Examples
    </th>
    
    <th>
      SEO Treatment
    </th>
    
    <th>
      Why
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Filters
      </strong>
    </td>
    
    <td>
      <code>
        ?color=red&size=large
      </code>
    </td>
    
    <td>
      Canonical to base or noindex
    </td>
    
    <td>
      Creates duplicates, thin content
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Sorting
      </strong>
    </td>
    
    <td>
      <code>
        ?sort=price
      </code>
    </td>
    
    <td>
      Include in canonical
    </td>
    
    <td>
      Changes value, users link to sorted views
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Pagination
      </strong>
    </td>
    
    <td>
      <code>
        ?page=2
      </code>
    </td>
    
    <td>
      Self-referencing canonical
    </td>
    
    <td>
      Each page has unique content
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Tracking
      </strong>
    </td>
    
    <td>
      <code>
        ?utm_source=twitter
      </code>
    </td>
    
    <td>
      Strip from canonical
    </td>
    
    <td>
      No content value, analytics only
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Search
      </strong>
    </td>
    
    <td>
      <code>
        ?q=shoes
      </code>
    </td>
    
    <td>
      Depends on results
    </td>
    
    <td>
      Index if unique results, noindex if duplicates
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Sessions
      </strong>
    </td>
    
    <td>
      <code>
        ?sessionid=abc
      </code>
    </td>
    
    <td>
      Canonical to base
    </td>
    
    <td>
      Creates infinite URLs
    </td>
  </tr>
</tbody>
</table>

## Filter Parameters

Filters create exponential URL variations. Three color filters generate 8 combinations (red, blue, green, red+blue, red+green, blue+green, red+blue+green, none).

### Block Filtered Pages

```ts [pages/Products.vue]
const route = useRoute()
const hasFilters = computed(() =>
  route.query.color || route.query.size || route.query.brand
)

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

useSeoMeta({
  robots: hasFilters.value ? 'noindex, follow' : 'index, follow'
})
```

### Move Important Filters to Path

For SEO-valuable filters (categories, main attributes), use route paths instead of query params:

<code-group>

```ts [❌ Query Parameters]
// /products?category=shoes
const routes = [{
  path: '/products',
  component: Products
}]
```

```ts [✅ Path Segments]
// /products/shoes
const routes = [{
  path: '/products/:category',
  component: Products
}]
```

</code-group>

## Sort Parameters

Sorting changes presentation but not content. Users share sorted URLs ("cheapest laptops" links to `?sort=price`).

### Include Sort in Canonical

```ts [pages/Products.vue]
const route = useRoute()
const allowedSortValues = ['price', 'name', 'date', 'rating']
const sort = computed(() =>
  allowedSortValues.includes(route.query.sort)
    ? route.query.sort
    : undefined
)

useHead({
  link: [{
    rel: 'canonical',
    href: sort.value
      ? `https://mysite.com/products?sort=${sort.value}`
      : 'https://mysite.com/products'
  }]
})
```

**Why validate sort values?** Prevents parameter manipulation creating infinite URLs (`?sort=abc`, `?sort=xyz`).

### Block Sort from Indexing

If sorted views don't add value (same content, different order), use noindex:

```ts
useSeoMeta({
  robots: route.query.sort ? 'noindex, follow' : 'index, follow'
})
```

## Pagination Parameters

Each page in a sequence has unique content. [Google recommends self-referencing canonicals](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading). don't point page 2 to page 1. See the [Pagination SEO guide](/learn-seo/vue/routes-and-rendering/pagination) for full implementation details.

```ts [pages/Blog.vue]
const route = useRoute()
const page = computed(() => route.query.page || '1')

useHead({
  link: [{
    rel: 'canonical',
    // Each page references itself
    href: page.value === '1'
      ? 'https://mysite.com/blog'
      : `https://mysite.com/blog?page=${page.value}`
  }]
})
```

### Validate Page Numbers

```ts
const page = computed(() => {
  const pageNum = Number.parseInt(route.query.page)
  return pageNum > 0 && pageNum <= maxPages ? pageNum : 1
})
```

Prevents crawlers requesting `?page=999999` and wasting server resources.

## Tracking Parameters

Analytics parameters (utm_, fbclid, gclid) don't change content but create duplicate URLs.

### Strip Tracking Params

```ts [composables/useCanonicalUrl.ts]
export function useCanonicalUrl(path: string) {
  const siteUrl = import.meta.env.VITE_SITE_URL
  const route = useRoute()

  // List of tracking params to ignore
  const trackingParams = [
    'utm_source',
    'utm_medium',
    'utm_campaign',
    'utm_term',
    'utm_content',
    'fbclid',
    'gclid',
    'msclkid',
    'mc_cid',
    'mc_eid',
    '_ga',
    'ref',
    'source'
  ]

  // Keep only non-tracking params
  const cleanParams = Object.fromEntries(
    Object.entries(route.query).filter(([key]) =>
      !trackingParams.includes(key)
    )
  )

  const queryString = new URLSearchParams(cleanParams).toString()

  return {
    link: [{
      rel: 'canonical',
      href: queryString
        ? `${siteUrl}${path}?${queryString}`
        : `${siteUrl}${path}`
    }]
  }
}
```

### Server-Side Parameter Handling

Redirect tracking parameters at the server level for proper 301 status codes:

<code-group>

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

const app = express()

app.use((req, res, next) => {
  const trackingParams = ['utm_source', 'fbclid', 'gclid']
  const hasTracking = trackingParams.some(param => req.query[param])

  if (hasTracking) {
    const cleanQuery = Object.fromEntries(
      Object.entries(req.query).filter(([key]) =>
        !trackingParams.includes(key)
      )
    )
    const queryString = new URLSearchParams(cleanQuery).toString()
    return res.redirect(301, `${req.path}${queryString ? `?${queryString}` : ''}`)
  }
  next()
})
```

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

const app = express()

app.use((req, res, next) => {
  const trackingParams = ['utm_source', 'fbclid', 'gclid']
  const hasTracking = trackingParams.some(param => req.query[param])

  if (hasTracking) {
    const cleanQuery = Object.fromEntries(
      Object.entries(req.query).filter(([key]) =>
        !trackingParams.includes(key)
      )
    )
    const queryString = new URLSearchParams(cleanQuery).toString()
    return res.redirect(301, `${req.path}${queryString ? `?${queryString}` : ''}`)
  }
  next()
})
```

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

export default defineEventHandler((event) => {
  const query = getQuery(event)
  const trackingParams = ['utm_source', 'fbclid', 'gclid']
  const hasTracking = trackingParams.some(param => query[param])

  if (hasTracking) {
    const cleanQuery = Object.fromEntries(
      Object.entries(query).filter(([key]) =>
        !trackingParams.includes(key)
      )
    )
    const queryString = new URLSearchParams(cleanQuery).toString()
    return sendRedirect(event, `${event.path}${queryString ? `?${queryString}` : ''}`, 301)
  }
})
```

</code-group>

## Parameter Order Consistency

`?sort=price&filter=red` and `?filter=red&sort=price` are identical content but different URLs. Enforce consistent parameter ordering:

```ts [composables/useCanonicalUrl.ts]
// eslint-disable-next-line harlanzw/vue-no-faux-composables
export function useCanonicalUrl(path: string, params: Record<string, string>) {
  const siteUrl = import.meta.env.VITE_SITE_URL

  // Define parameter order
  const paramOrder = ['category', 'sort', 'filter', 'page']

  // Sort params by predefined order
  const orderedParams = Object.fromEntries(
    Object.entries(params)
      .sort(([a], [b]) => {
        const indexA = paramOrder.indexOf(a)
        const indexB = paramOrder.indexOf(b)
        if (indexA === -1)
          return 1
        if (indexB === -1)
          return -1
        return indexA - indexB
      })
  )

  const queryString = new URLSearchParams(orderedParams).toString()

  return {
    link: [{
      rel: 'canonical',
      href: queryString
        ? `${siteUrl}${path}?${queryString}`
        : `${siteUrl}${path}`
    }]
  }
}
```

### Vue Router Navigation

Force parameter order when updating query strings:

```ts
import { useRouter } from 'vue-router'

const router = useRouter()

function updateFilters(filters: Record<string, string>) {
  const paramOrder = ['category', 'sort', 'filter', 'page']

  const ordered = Object.fromEntries(
    Object.entries(filters)
      .sort(([a], [b]) => {
        const indexA = paramOrder.indexOf(a)
        const indexB = paramOrder.indexOf(b)
        return indexA - indexB
      })
  )

  navigateTo({
    query: ordered
  })
}
```

## Search Parameters

Search queries create unique URLs for each search term. Treatment depends on result quality:

### Block Thin Search Results

```ts [pages/Search.vue]
const route = useRoute()
const query = route.query.q
const results = await searchProducts(query)

useSeoMeta({
  robots: !query || results.length < 5
    ? 'noindex, follow'
    : 'index, follow'
})

useHead({
  link: [{
    rel: 'canonical',
    href: query
      ? `https://mysite.com/search?q=${encodeURIComponent(query)}`
      : 'https://mysite.com/search'
  }]
})
```

### robots.txt for Search

Block crawlers from search entirely:

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

# Or use URL patterns
Disallow: /*?q=
Disallow: /*?query=
```

## Testing Parameter Handling

### Google Search Console

1. URL Inspection tool
2. Enter URL with parameters
3. Check "User-declared canonical" vs "Google-selected canonical"
4. Verify they match your preference

### Manual Verification

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

# Should return:
# <link rel="canonical" href="https://mysite.com/products?sort=price">
```

### Test Parameter Variations

Create a test matrix:

<table>
<thead>
  <tr>
    <th>
      URL
    </th>
    
    <th>
      Expected Canonical
    </th>
    
    <th>
      Expected Robots
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        /products
      </code>
    </td>
    
    <td>
      Self
    </td>
    
    <td>
      index, follow
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        /products?sort=price
      </code>
    </td>
    
    <td>
      Self or base
    </td>
    
    <td>
      Depends on strategy
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        /products?filter=red
      </code>
    </td>
    
    <td>
      Base URL
    </td>
    
    <td>
      noindex, follow
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        /products?utm_source=twitter
      </code>
    </td>
    
    <td>
      Base URL
    </td>
    
    <td>
      index, follow
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        /products?page=2
      </code>
    </td>
    
    <td>
      Self
    </td>
    
    <td>
      index, follow
    </td>
  </tr>
</tbody>
</table>

## robots.txt Parameter Blocking

Block specific parameters from crawling entirely:

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

# Block all URLs with these params
Disallow: /*?sessionid=
Disallow: /*?sid=
Disallow: /*&sessionid=
Disallow: /*&sid=

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

# Block filter combinations
Disallow: /*?filter=
Disallow: /*&filter=
```

[Google deprecated parameter handling in Search Console](https://developers.google.com/search/blog/2022/06/retiring-url-parameters-tool) in 2022. Use robots.txt or meta robots instead.

## Common Mistakes

**Using client-side canonicals for SPAs:** Googlebot doesn't execute JavaScript fast enough. Server-render canonical tags or use SSR.

**Indexing every parameter variation:** Creates thin content and wastes crawl budget. Pick one canonical version.

**Inconsistent parameter handling:** Some pages canonical to base, others to self. Be consistent site-wide.

**Ignoring tracking parameters:** Analytics params create duplicate URLs. Strip them from canonicals.

**Not validating parameter values:** Allows `?sort=anything` creating infinite URLs. Whitelist valid values.

## Using Nuxt?

If you're using Nuxt, it provides automatic canonical URL handling.

[Learn more about query parameters in Nuxt →](/learn-seo/nuxt/routes-and-rendering)
