- Published on
SEO-Aware Links: Automatic nofollow for Query Parameters in Nuxt
- Authors
The SEO Problem with Query Parameters
E-commerce sites have thousands of URLs. A product category page might be:
/products/kitchen-sinks ← Canonical
/products/kitchen-sinks?color=white ← Filtered
/products/kitchen-sinks?material=steel ← Filtered
/products/kitchen-sinks?sort=price-asc ← Sorted
/products/kitchen-sinks?page=2 ← Paginated
Google crawls all of these. If internal links point to filtered versions, you're diluting PageRank across dozens of near-duplicate URLs instead of concentrating it on the canonical page.
The SEO fix: add rel="nofollow" to links with query parameters (except pagination, which you want indexed).
The Manual Approach (Tedious)
<!-- Everywhere in your codebase -->
<NuxtLink :to="{ path: '/products', query: { color: 'white' } }" rel="nofollow">
White Products
</NuxtLink>
<NuxtLink :to="{ path: '/products', query: { page: 2 } }">
Page 2 <!-- No nofollow for pagination -->
</NuxtLink>
This requires remembering to add rel="nofollow" to every filtered link. Developers will forget. SEO will suffer.
The Automated Solution
Create a wrapper component that automatically adds nofollow when query parameters are present:
<!-- components/CustomNuxtLink.vue -->
<script setup lang="ts">
import type { NuxtLinkProps } from '#app'
const props = defineProps<NuxtLinkProps>()
const computedRel = computed(() => {
if (!props.to) {
return props.rel
}
let hasQueryParams = false
// Handle object-style :to prop
if (typeof props.to === 'object' && props.to !== null) {
const query = props.to.query
if (query && typeof query === 'object') {
const queryKeys = Object.keys(query)
if (queryKeys.length > 0) {
// Exception: pagination-only links should NOT be nofollow
const onlyPage = queryKeys.length === 1 && queryKeys[0] === 'page'
hasQueryParams = !onlyPage
}
}
}
// Handle string-style :to prop
else if (typeof props.to === 'string') {
const hasQuery = props.to.includes('?')
if (hasQuery) {
const urlParams = new URLSearchParams(props.to.split('?')[1])
const paramKeys = Array.from(urlParams.keys())
const onlyPage = paramKeys.length === 1 && paramKeys[0] === 'page'
hasQueryParams = !onlyPage
}
}
if (!hasQueryParams) {
return props.rel
}
// Add nofollow, preserving any existing rel value
return props.rel ? `${props.rel} nofollow` : 'nofollow'
})
</script>
<template>
<NuxtLink v-bind="{ ...props, rel: computedRel }">
<slot />
</NuxtLink>
</template>
How It Works
Regular Links: No Change
<CustomNuxtLink to="/products">Products</CustomNuxtLink>
<!-- Renders: <a href="/products">Products</a> -->
<CustomNuxtLink :to="{ name: 'products-category', params: { category: 'sinks' } }">
Sinks
</CustomNuxtLink>
<!-- Renders: <a href="/products/sinks">Sinks</a> -->
Filtered Links: Automatic nofollow
<CustomNuxtLink :to="{ path: '/products', query: { color: 'white' } }">
White Products
</CustomNuxtLink>
<!-- Renders: <a href="/products?color=white" rel="nofollow">White Products</a> -->
<CustomNuxtLink to="/products?material=steel">
Steel Products
</CustomNuxtLink>
<!-- Renders: <a href="/products?material=steel" rel="nofollow">Steel Products</a> -->
Pagination: Exempt from nofollow
<CustomNuxtLink :to="{ path: '/products', query: { page: 2 } }">
Page 2
</CustomNuxtLink>
<!-- Renders: <a href="/products?page=2">Page 2</a> -->
<!-- No nofollow! Pagination should be indexed -->
Mixed Parameters: Gets nofollow
<CustomNuxtLink :to="{ path: '/products', query: { color: 'white', page: 2 } }">
White Products - Page 2
</CustomNuxtLink>
<!-- Renders: <a href="/products?color=white&page=2" rel="nofollow">...</a> -->
<!-- Has nofollow because color param is present -->
Migration Strategy
Step 1: Create the Component
Save CustomNuxtLink.vue in your components/ directory.
Step 2: Global Find & Replace
Replace all <NuxtLink with <CustomNuxtLink:
# Find all occurrences
grep -r "<NuxtLink" src/
# Or use your IDE's find & replace
# From: <NuxtLink
# To: <CustomNuxtLink
Step 3: Update Closing Tags
# From: </NuxtLink>
# To: </CustomNuxtLink>
Step 4: Verify
Check that links render correctly:
# Build and inspect HTML
npm run build
grep -r 'rel="nofollow"' .output/
Extending the Pattern
External Links: Always nofollow
<script setup lang="ts">
const computedRel = computed(() => {
// ... existing query param logic ...
// External links always get nofollow
if (typeof props.to === 'string' && props.to.startsWith('http')) {
return props.rel ? `${props.rel} nofollow noopener` : 'nofollow noopener'
}
// ... rest of logic ...
})
</script>
API/Asset Links: nofollow + noindex
<script setup lang="ts">
const computedRel = computed(() => {
const to = props.to?.toString() ?? ''
// API and asset links
if (to.includes('/api/') || to.includes('/assets/')) {
return props.rel ? `${props.rel} nofollow noindex` : 'nofollow noindex'
}
// ... existing logic ...
})
</script>
Specific Paths to Exclude
<script setup lang="ts">
const nofollowPaths = ['/admin', '/dashboard', '/preview']
const computedRel = computed(() => {
const to = props.to?.toString() ?? ''
// Always nofollow certain paths
if (nofollowPaths.some((path) => to.startsWith(path))) {
return 'nofollow'
}
// ... existing logic ...
})
</script>
Edge Cases Handled
Preserving Existing rel Values
<CustomNuxtLink :to="{ path: '/docs', query: { tab: 'api' } }" rel="external">
API Docs
</CustomNuxtLink>
<!-- Renders: rel="external nofollow" -->
Empty Query Objects
<CustomNuxtLink :to="{ path: '/products', query: {} }">
Products
</CustomNuxtLink>
<!-- Renders: no rel (empty query doesn't count) -->
Null/Undefined to Prop
<CustomNuxtLink :to="null">
Disabled Link
</CustomNuxtLink>
<!-- Handles gracefully, returns original rel -->
TypeScript Benefits
Using NuxtLinkProps ensures full type safety:
import type { NuxtLinkProps } from '#app'
const props = defineProps<NuxtLinkProps>()
All NuxtLink props work automatically:
to(string | RouteLocationRaw)reltargetprefetchactiveClassexactActiveClass- etc.
Performance Considerations
The computedRel runs once per link on render. For pages with hundreds of links, this is still negligible—it's just string parsing.
If you're paranoid:
<script setup lang="ts">
// Memoize the result if to/rel don't change
const computedRel = computed(() => {
// ... logic ...
})
// Or use a static utility for SSR
const rel = computed(() => calculateRel(props.to, props.rel))
</script>
Verifying SEO Impact
Before: Check Current Links
# Crawl your site and count nofollow links
curl -s https://yoursite.com | grep -c 'rel="nofollow"'
After: Compare
After migration, the count should increase significantly on filter-heavy pages.
Google Search Console
Monitor "Discovered - currently not indexed" in Coverage report. Fewer filtered URLs should appear as Google respects nofollow.
The 89-File Refactor
When we implemented this, we touched 89 files. The migration took about an hour:
- Create component (5 minutes)
- Find & replace (10 minutes)
- Build and test (30 minutes)
- Fix edge cases (15 minutes)
The SEO benefit is permanent. Developers no longer need to remember nofollow—it's automated.
Complete Component
<!-- components/CustomNuxtLink.vue -->
<script setup lang="ts">
import type { NuxtLinkProps } from '#app'
const props = defineProps<NuxtLinkProps>()
const computedRel = computed(() => {
if (!props.to) {
return props.rel
}
let hasQueryParams = false
if (typeof props.to === 'object' && props.to !== null) {
const query = props.to.query
if (query && typeof query === 'object') {
const queryKeys = Object.keys(query)
if (queryKeys.length > 0) {
const onlyPage = queryKeys.length === 1 && queryKeys[0] === 'page'
hasQueryParams = !onlyPage
}
}
} else if (typeof props.to === 'string') {
const hasQuery = props.to.includes('?')
if (hasQuery) {
const urlParams = new URLSearchParams(props.to.split('?')[1])
const paramKeys = Array.from(urlParams.keys())
const onlyPage = paramKeys.length === 1 && paramKeys[0] === 'page'
hasQueryParams = !onlyPage
}
}
if (!hasQueryParams) {
return props.rel
}
return props.rel ? `${props.rel} nofollow` : 'nofollow'
})
</script>
<template>
<NuxtLink v-bind="{ ...props, rel: computedRel }">
<slot />
</NuxtLink>
</template>
Drop it in, replace your links, and never worry about nofollow on filtered URLs again.
Real component from an e-commerce site with thousands of product filter combinations. 89 files refactored, SEO improved automatically.