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.
Handle query parameters with canonical URLs to tell search engines which version to index:
<script setup>
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>
<script setup>
useHead({
link: [{
rel: 'canonical',
// Always canonical to base URL
href: 'https://mysite.com/products'
}]
})
</script>
<script setup>
const route = useRoute()
// Use noindex for filtered/sorted views
useSeoMeta({
robots: computed(() =>
route.query.filter || route.query.page
? 'noindex, follow'
: 'index, follow'
)
})
</script>
Different query parameters need different handling:
| Parameter Type | Examples | SEO Treatment | Why |
|---|---|---|---|
| Filters | ?color=red&size=large | Canonical to base or noindex | Creates duplicates, thin content |
| Sorting | ?sort=price | Include in canonical | Changes value, users link to sorted views |
| Pagination | ?page=2 | Self-referencing canonical | Each page has unique content |
| Tracking | ?utm_source=twitter | Strip from canonical | No content value, analytics only |
| Search | ?q=shoes | Depends on results | Index if unique results, noindex if duplicates |
| Sessions | ?sessionid=abc | Canonical to base | Creates infinite URLs |
Filters create exponential URL variations. Three color filters generate 8 combinations (red, blue, green, red+blue, red+green, blue+green, red+blue+green, none).
<script setup>
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>
For SEO-valuable filters (categories, main attributes), use route paths instead of query params:
// /products?category=shoes
// Not ideal for important categories
// /products/shoes
// Create pages/products/[category].vue
Sorting changes presentation but not content. Users share sorted URLs ("cheapest laptops" links to ?sort=price).
<script setup>
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).
If sorted views don't add value (same content, different order), use noindex:
useSeoMeta({
robots: route.query.sort ? 'noindex, follow' : 'index, follow'
})
Each page in a sequence has unique content. Google recommends self-referencing canonicals—don't point page 2 to page 1.
<script setup>
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>
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.
Analytics parameters (utm_, fbclid, gclid) don't change content but create duplicate URLs.
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}`
}]
}
}
Nuxt handles tracking parameter redirects through route rules:
export default defineNuxtConfig({
routeRules: {
'/**': {
redirect: {
// Strip tracking params via middleware
}
}
}
})
Or use server middleware:
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)
}
})
?sort=price&filter=red and ?filter=red&sort=price are identical content but different URLs. Enforce consistent parameter ordering:
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}`
}]
}
}
Force parameter order when updating query strings:
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
})
)
router.push({
query: ordered
})
}
Search queries create unique URLs for each search term. Treatment depends on result quality:
<script setup>
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>
Block crawlers from search entirely:
User-agent: *
Disallow: /search?
# Or use URL patterns
Disallow: /*?q=
Disallow: /*?query=
# 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">
Create a test matrix:
| URL | Expected Canonical | Expected Robots |
|---|---|---|
/products | Self | index, follow |
/products?sort=price | Self or base | Depends on strategy |
/products?filter=red | Base URL | noindex, follow |
/products?utm_source=twitter | Base URL | index, follow |
/products?page=2 | Self | index, follow |
Block specific parameters from crawling entirely:
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 in 2022. Use robots.txt or meta robots instead.
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.