Kamil Owczarek
Opublikowano

Linki świadome SEO: automatyczny nofollow dla parametrów query w Nuxt

Autorzy

Problem SEO z parametrami query

Sklepy e-commerce mają tysiące URL-i. Strona kategorii produktowej może wyglądać tak:

/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=2Paginated

Google crawluje je wszystkie. Jeśli linki wewnętrzne prowadzą do przefiltrowanych wersji, rozcieńczasz PageRank na dziesiątki niemal identycznych URL-i, zamiast skupić go na stronie kanonicznej.

Poprawka po stronie SEO: dodać rel="nofollow" do linków z parametrami query (poza paginacją, którą chcesz mieć zaindeksowaną).

Podejście ręczne (żmudne)

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

Wymaga to pamiętania o dodaniu rel="nofollow" do każdego przefiltrowanego linku. Developerzy zapomną. SEO na tym ucierpi.

Rozwiązanie zautomatyzowane

Stwórz komponent-wrapper, który automatycznie dodaje nofollow, gdy obecne są parametry query:

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

Jak to działa

Zwykłe linki: bez zmian

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

Linki przefiltrowane: automatyczny 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> -->

Paginacja: zwolniona z 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 -->

Parametry mieszane: dostają 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 -->

Strategia migracji

Krok 1: stwórz komponent

Zapisz CustomNuxtLink.vue w katalogu components/.

Krok 2: globalne znajdź i zamień

Zamień wszystkie <NuxtLink na <CustomNuxtLink:

# Find all occurrences
grep -r "<NuxtLink" src/

# Or use your IDE's find & replace
# From: <NuxtLink
# To: <CustomNuxtLink

Krok 3: zaktualizuj tagi zamykające

# From: </NuxtLink>
# To: </CustomNuxtLink>

Krok 4: zweryfikuj

Sprawdź, czy linki renderują się poprawnie:

# Build and inspect HTML
npm run build
grep -r 'rel="nofollow"' .output/

Rozszerzanie wzorca

Linki zewnętrzne: zawsze 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>

Linki do API i assetów: 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>

Konkretne ścieżki do wykluczenia

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

Obsłużone edge case'y

Zachowanie istniejących wartości rel

<CustomNuxtLink :to="{ path: '/docs', query: { tab: 'api' } }" rel="external">
  API Docs
</CustomNuxtLink>
<!-- Renders: rel="external nofollow" -->

Puste obiekty query

<CustomNuxtLink :to="{ path: '/products', query: {} }">
  Products
</CustomNuxtLink>
<!-- Renders: no rel (empty query doesn't count) -->

Prop to równy null lub undefined

<CustomNuxtLink :to="null">
  Disabled Link
</CustomNuxtLink>
<!-- Handles gracefully, returns original rel -->

Korzyści z TypeScript

Użycie NuxtLinkProps daje pełne bezpieczeństwo typów:

import type { NuxtLinkProps } from '#app'

const props = defineProps<NuxtLinkProps>()

Wszystkie propsy NuxtLink działają automatycznie:

  • to (string | RouteLocationRaw)
  • rel
  • target
  • prefetch
  • activeClass
  • exactActiveClass
  • i tak dalej

Kwestie wydajnościowe

computedRel uruchamia się raz na link przy renderowaniu. Nawet dla stron z setkami linków jest to pomijalne — to zwykłe parsowanie stringów.

A jeśli masz paranoję:

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

Weryfikacja wpływu na SEO

Przed: sprawdź obecne linki

# Crawl your site and count nofollow links
curl -s https://yoursite.com | grep -c 'rel="nofollow"'

Po: porównaj

Po migracji liczba ta powinna wyraźnie wzrosnąć na stronach z dużą liczbą filtrów.

Google Search Console

Obserwuj "Discovered - currently not indexed" w raporcie Coverage. Powinno pojawiać się mniej przefiltrowanych URL-i, bo Google respektuje nofollow.

Refaktor 89 plików

Kiedy to wdrażaliśmy, ruszyliśmy 89 plików. Migracja zajęła około godziny:

  1. Stworzenie komponentu (5 minut)
  2. Znajdź i zamień (10 minut)
  3. Build i testy (30 minut)
  4. Poprawki edge case'ów (15 minut)

Korzyść dla SEO jest trwała. Developerzy nie muszą już pamiętać o nofollow — dzieje się to automatycznie.

Kompletny komponent

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

Wrzuć go do projektu, podmień linki i już nigdy nie martw się o nofollow na przefiltrowanych URL-ach.


Prawdziwy komponent ze sklepu e-commerce z tysiącami kombinacji filtrów produktowych. 89 plików zrefaktorowanych, SEO poprawione automatycznie.