Przejdź do treści

Hreflang i link kanoniczny w Next.js — SEO wielojęzycznych stron bez duplikacji

Błędny hreflang lub link kanoniczny w Next.js generuje tysiące zduplikowanych stron. Jak uniknąć tego w App Router przy wielojęzycznej strukturze?

Maciej Sala

Founder StriveLab

4 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 25 maja 2026)

Czym jest hreflang i dlaczego jest krytyczny w SEO wielojęzycznym?

to atrybut HTML, który informuje Google, która wersja językowa strony jest przeznaczona dla danego regionu lub języka. Bez hreflang Google może wyświetlić polską wersję użytkownikowi anglojęzycznemu lub potraktować wersje językowe jako duplikaty (kanibalizacja). Może też indeksować tylko jedną wersję. Krótko mówiąc: bez hreflang czeka nas bałagan, podczas gdy hreflang wprowadza kontrolę i porządek.

(link rel="canonical") wskazuje „źródłową" wersję strony i w ten sposób eliminuje duplikaty wynikające z parametrów URL, trailing slashy, czy wariantów http/https. Oba tagi "współpracują" ze sobą, ponieważ hreflang łączy wersje językowe, link kanoniczny wyznacza kanoniczną wersję w każdym języku.

Hreflang w Next.js App Router przez Metadata API

Code
// app/[locale]/page.tsx
import type { Metadata } from 'next'
 
const locales = ['pl', 'en', 'de']
const baseUrl = 'https://example.com'
 
// Polska wersja na rootu domeny, pozostałe języki z prefiksem
const getLocaleUrl = (locale: string, path = '') =>
  locale === 'pl' ? `${baseUrl}${path}` : `${baseUrl}/${locale}${path}`
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string }>
}): Promise<Metadata> {
  const { locale } = await params
 
  return {
    alternates: {
      canonical: getLocaleUrl(locale),
      languages: {
        pl: baseUrl,
        en: `${baseUrl}/en`,
        de: `${baseUrl}/de`,
        'x-default': baseUrl,
      },
    },
  }
}

Wynikowy HTML:

Code
<link rel="canonical" href="https://example.com/" />
<link rel="alternate" hreflang="pl" href="https://example.com/" />
<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="de" href="https://example.com/de" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />

Polska wersja działa na rootu domeny (https://example.com/) bez prefiksu /pl — domena .pl już precyzuje rynek. Pozostałe języki mają własny prefiks (/en, /de). Kluczową zasadą jest by link kanoniczny, hreflang, i linkowanie wewnętrzne były konsekwentnie używane wobec tego samego wariantu URL.

Diagram
Poprawna relacja linku kanonicznego i hreflang dla wersji językowych

Hreflang dla dynamicznych stron z parametrami

Code
// app/[locale]/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string; slug: string }>
}): Promise<Metadata> {
  const { locale, slug } = await params
  const post = await getPost(slug, locale)
 
  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: getLocaleUrl(locale, `/blog/${slug}`),
      languages: Object.fromEntries(
        locales.map((l) => [
          l,
          getLocaleUrl(l, `/blog/${post.slugs[l] || slug}`),
        ]),
      ),
    },
  }
}

Jeśli slugi są tłumaczone (/pl/blog/jak-zbudowac-strone vs /en/blog/how-to-build-website), każda wersja językowa musi wskazywać na poprawny slug w danym języku.

Code
// next.config.ts
const nextConfig = {
  trailingSlash: false, // Wymuś brak trailing slash
}

Dodatkowo w pliku middleware.ts:

Code
if (pathname.length > 1 && pathname.endsWith('/')) {
  return NextResponse.redirect(new URL(pathname.slice(0, -1), request.url), 308)
}

Strona /products i /products?sort=price to ta sama treść. Link kanoniczny powinien wskazywać wersję bez parametrów:

Code
export const metadata: Metadata = {
  alternates: {
    canonical: '/products', // Bez ?sort=, ?page=, ?filter=
  },
}

www vs non-www w linkach kanonicznych

Wybierz jedną wersję i przekieruj drugą. W Vercel — konfiguracja domeny. Na VPS — nginx redirect:

Code
server {
    server_name www.strivelab.pl;
    return 301 https://example.com$request_uri;
}

Helper do generowania alternates, hreflang i linku kanonicznego

Code
// lib/seo.ts
const baseUrl = 'https://example.com'
const locales = ['pl', 'en'] as const
 
const getLocaleUrl = (locale: (typeof locales)[number], path: string) =>
  locale === 'pl' ? `${baseUrl}${path}` : `${baseUrl}/${locale}${path}`
 
export function generateAlternates(
  locale: (typeof locales)[number],
  localizedPaths: Record<(typeof locales)[number], string>,
) {
  const languages = Object.fromEntries(
    locales.map((l) => [l, getLocaleUrl(l, localizedPaths[l])]),
  )
 
  return {
    canonical: getLocaleUrl(locale, localizedPaths[locale]),
    languages: {
      ...languages,
      'x-default': `${baseUrl}${localizedPaths.pl}`,
    },
  }
}
Code
// Użycie
export async function generateMetadata(): Promise<Metadata> {
  return {
    alternates: generateAlternates('pl', {
      pl: '/uslugi',
      en: '/services',
    }),
  }
}

Sitemap wielojęzyczna z alternates i hreflang

Code
// app/sitemap.ts
import type { MetadataRoute } from 'next'
 
export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://example.com'
  const locales = ['pl', 'en'] as const
 
  const getLocaleUrl = (locale: string, path: string) =>
    locale === 'pl' ? `${baseUrl}${path}` : `${baseUrl}/${locale}${path}`
 
  const pages = [
    { plPath: '', enPath: '' },
    { plPath: '/uslugi', enPath: '/services' },
    { plPath: '/kontakt', enPath: '/contact' },
    { plPath: '/blog', enPath: '/blog' },
  ]
 
  return pages.flatMap((page) =>
    locales.map((locale) => ({
      url: getLocaleUrl(locale, locale === 'pl' ? page.plPath : page.enPath),
      lastModified: new Date(),
      alternates: {
        languages: Object.fromEntries(
          locales.map((l) => [
            l,
            getLocaleUrl(l, l === 'pl' ? page.plPath : page.enPath),
          ]),
        ),
      },
    })),
  )
}

Typowe błędy hreflang i linku kanonicznego w Next.js

Brakujący x-default

x-default wskazuje wersję dla użytkowników, których język nie jest obsługiwany. Warto zaznaczyć, że nie jest to obowiązkowe w każdym projekcie, ale zwykle warto go dodać, jeśli masz stronę domyślną albo selektor języka.

Jeśli strona PL wskazuje na EN, to EN musi wskazywać z powrotem na PL. W wypadku braku wzajemności = Google ignoruje hreflang.

Link kanoniczny powinien wskazywać na siebie (w danym języku), nie na inną wersję językową. Każda wersja językowa ma własny link kanoniczny.

Hreflang na stronach z noindex

Jeśli strona ma atrybut noindex, wtedy hreflang jest ignorowany.

  • Google Search Console → inspekcja konkretnych URL-i i raport indeksowania,
  • Ahrefs / Screaming Frog — audyt hreflang na dużą skalę,
  • Ręczne sprawdzenie — View Source → szukaj rel="alternate" hreflang.
Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czy hreflang zastępuje link kanoniczny?

Nie. Hreflang wskazuje alternatywne wersje językowe lub regionalne, a link kanoniczny wskazuje kanoniczny adres danej wersji. Oba mechanizmy powinny ze sobą współpracować.

Czy każda wersja językowa musi linkować do pozostałych?

Tak, konfiguracja hreflang powinna być wzajemna. Jeśli wersja polska wskazuje angielską, wersja angielska powinna wskazywać polską oraz samą siebie.

Gdzie najlepiej obsłużyć hreflang w Next.js?

Najczęściej w Metadata API oraz w sitemapie, zależnie od struktury routingu. Kluczowe jest, żeby adresy były spójne z linkami kanonicznymi i realnie dostępne.

Czy hreflang wpływa na ranking?

Hreflang nie jest czynnikiem rankingowym i nie ma wpływu na poprawę pozycji, ale poprawia trafność wyświetlanej wersji, co wpływa na CTR i doświadczenie użytkownika. Jeśli miałby jakikolwiek wpływ to pośredni.

Czy muszę mieć osobne domeny dla języków?

Nie, podfoldery (/pl/, /en/) z hreflang to rekomendowane przez Google podejście, a osobne domeny (.pl, .com) to opcja raczej dla dużych marek.

Co jeśli mam tylko jeden język?

Nie potrzebujesz hreflang, jedynie ustaw link kanoniczny na każdej stronie — eliminuje duplikaty z parametrów URL i trailing slashy.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
QA w technicznym SEO: link kanoniczny, hreflang, metadata i redirecty bez regresji

Najbardziej kosztowne błędy SEO uderzają cicho i niepostrzeżenie. Strona bezproblemowo się ładuje, formularz dalej działa, a użytkownik normalnie z niej korzysta. Jednocześnie Google zaczyna dostawać nieprawidłowy link kanoniczny, brakujący hreflang albo nadany przypadkowo noindex . QA w SEO polega na wyraźnych regułach w kodzie, testach i walidacji danych.

Maciej Sala

Maciej Sala

Founder StriveLab

Wielojęzyczność i hreflang w Astro SSG: Zasięg globalny, wdrożenie lokalne

Zanim Twoja oferta dotrze do zagranicznych klientów, Twoja strona musi zdać egzamin techniczny. Większość witryn odpada na tym etapie, marnując potencjał nawet najlepszej treści. Sprawdź, jak rygorystycznie skonfigurować wielojęzyczność i hreflang na statycznym Astro, by zasięg globalny szedł w parze z lokalną, błyskawiczną dostawą treści.

Maciej Sala

Maciej Sala

Founder StriveLab

Parametry w URL a SEO: jak nie zduplikować treści w React, Next.js i Astro?

Parametry w URL mają, jak przysłowiowa moneta ma dwie strony. Z jednej, napędzają filtry, sortowanie, paginację i śledzenie kampanii, a z drugiej, jednocześnie potrafią cicho popsuć widoczność serwisu przez duplikacje treści, marnowany budżet indeksowania i rozwodnione link equity. W aplikacjach React, Next.js i Astro to problem architektoniczny i właśnie dlatego samo użycie rel="canonical" go nie rozwiązuje. W tym przewodniku przechodzę od klasyfikacji parametrów do decyzji o indeksacji, normalizacji URL-i, renderowania, paginacji oraz kontroli nawigacji fasetowej.

Maciej Sala

Maciej Sala

Founder StriveLab