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

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

Query parameters (`?sort=price&filter=red`) create duplicate content. 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:

```vue [pages/products.vue]
<script setup lang="ts">
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'
  }]
})
</script>
```

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

```vue [pages/products.vue - Block from Indexing]
<script setup lang="ts">
const route = useRoute()

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

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

```vue [pages/products.vue]
<script setup lang="ts">
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'
})
</script>
```

### 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
// Not ideal for important categories
```

```ts [✅ Path Segments]
// /products/shoes
// Create pages/products/[category].vue
```

</code-group>

## Sort Parameters

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

### Include Sort in Canonical

```vue [pages/products.vue]
<script setup lang="ts">
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'
  }]
})
</script>
```

**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/nuxt/routes-and-rendering/pagination) for full implementation details.

```vue [pages/blog.vue]
<script setup lang="ts">
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}`
  }]
})
</script>
```

### 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 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
        ? `${useSiteConfig().url}${path}?${queryString}`
        : `${useSiteConfig().url}${path}`
    }]
  }
}
```

### Server-Side Parameter Handling

Nuxt handles tracking parameter redirects through route rules:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  routeRules: {
    '/**': {
      redirect: {
        // Strip tracking params via middleware
      }
    }
  }
})
```

Or use server middleware:

```ts [server/middleware/tracking-params.ts]
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)
  }
})
```

## 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>) {
  // 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
        ? `${useSiteConfig().url}${path}?${queryString}`
        : `${useSiteConfig().url}${path}`
    }]
  }
}
```

### Navigation with Ordered Parameters

Force parameter order when updating query strings:

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

```vue [pages/search.vue]
<script setup lang="ts">
const route = useRoute()
const query = route.query.q
const { data: results } = await useFetch('/api/search', {
  query: { q: query }
})

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

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

### 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:** Nuxt renders canonical tags server-side by default, ensuring search engines see them immediately.

**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.

## Automate with Nuxt SEO Utils

[Nuxt SEO Utils](/docs/seo-utils/getting-started/introduction) handles canonical URL parameter stripping automatically. It removes tracking parameters (utm_*, fbclid, gclid) from canonical URLs without manual configuration.

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/seo'],
  // Canonical URLs automatically strip tracking params
})
```
