Przejdź do treści

Hreflang i link kanoniczny w Next.js dla stron i18n

Błędny hreflang lub link kanoniczny w Next.js wysyła Google sprzeczne sygnały. Zobacz, jak poprawnie wdrożyć je w App Router dla stron wielojęzycznych.

Maciej Sala

Founder StriveLab

5 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 30 lipca 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 albo dobrać niewłaściwy wariant regionalny. W pełni przetłumaczone strony nie są z tego powodu automatycznie duplikatami. Google uznaje wersje lokalizowane za duplikaty wtedy, gdy główna treść pozostaje nieprzetłumaczona. Hreflang pomaga wyszukiwarce powiązać odpowiedniki i wyświetlić właściwy URL.

(link rel="canonical") wskazuje preferowany, reprezentatywny adres spośród duplikatów lub bardzo podobnych wariantów, np. URL-i z parametrami. Jest silnym sygnałem, ale Google może wybrać inny adres, jeśli pozostałe sygnały są niespójne. Hreflang łączy wersje językowe, a link kanoniczny wskazuje preferowany URL w obrębie każdej z nich.

Hreflang w Next.js App Router przez Metadata API

Code
// app/[locale]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
 
const locales = ['pl', 'en', 'de'] as const
type Locale = (typeof locales)[number]
 
const baseUrl = 'https://example.com'
 
// Wszystkie języki korzystają z tej samej, jawnej struktury URL
const getLocaleUrl = (locale: Locale, path = '') =>
  `${baseUrl}/${locale}${path}`
 
const isLocale = (value: string): value is Locale =>
  locales.some((locale) => locale === value)
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string }>
}): Promise<Metadata> {
  const { locale } = await params
  if (!isLocale(locale)) notFound()
 
  return {
    alternates: {
      canonical: getLocaleUrl(locale),
      languages: {
        pl: `${baseUrl}/pl`,
        en: `${baseUrl}/en`,
        de: `${baseUrl}/de`,
        'x-default': `${baseUrl}/pl`,
      },
    },
  }
}

Wynikowy HTML dla żądania /pl:

Code
<link rel="canonical" href="https://example.com/pl" />
<link rel="alternate" hreflang="pl" href="https://example.com/pl" />
<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/pl" />

Ten przykład używa prefiksu dla każdego języka, więc trasa app/[locale]/page.tsx odpowiada adresom /pl, /en i /de. Możesz umieścić język domyślny na rootcie domeny, ale wtedy / wymaga osobnej trasy albo jawnego rewrite'u. Sam segment [locale] go nie obsłuży. Kluczowe jest, by link kanoniczny, hreflang, mapa witryny i linkowanie wewnętrzne konsekwentnie używały 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
  if (!isLocale(locale)) notFound()
 
  const post = await getPost(slug, locale)
 
  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: getLocaleUrl(locale, `/blog/${slug}`),
      languages: Object.fromEntries(
        locales.flatMap((l) => {
          const translatedSlug = post.slugs[l]
 
          return translatedSlug
            ? [[l, getLocaleUrl(l, `/blog/${translatedSlug}`)]]
            : []
        }),
      ),
    },
  }
}

Jeśli slugi są tłumaczone (/pl/blog/jak-zbudowac-strone vs /en/blog/how-to-build-website), każda wersja musi wskazywać na rzeczywiście opublikowany odpowiednik. Nie generuj hreflang z fallbackiem do bieżącego sluga, bo taki adres może zwracać 404 albo zawierać treść w niewłaściwym języku.

Code
// next.config.ts
const nextConfig = {
  trailingSlash: false, // Domyślne zachowanie: /about/ przekieruje na /about
}

Nie dodawaj drugiego redirectu w middleware.ts: Next.js obsługuje tę normalizację na podstawie trailingSlash. Od Next.js 16 konwencja middleware.ts jest dodatkowo przestarzała i została zastąpiona przez proxy.ts. Do prostych przekierowań najpierw używaj konfiguracji frameworka.

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

Code
// app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
}
Code
// app/products/page.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  alternates: {
    canonical: '/products', // metadataBase utworzy pełny URL
  },
}

Względny URL w polu canonical wymaga metadataBase, zwykle ustawionego w głównym layoucie. Bez niego Next.js zgłosi błąd podczas budowania.

www vs non-www w linkach kanonicznych

Wybierz jedną wersję i przekieruj drugą. W Vercel użyj konfiguracji domeny. Na VPS sprawdzi się nginx redirect:

Code
server {
    server_name www.example.com;
    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) =>
  `${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': getLocaleUrl('pl', 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

Jeśli wybierasz implementację hreflang w sitemapie, każdy URL powinien zawierać ten sam komplet istniejących odpowiedników, włącznie z samym sobą:

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) =>
    `${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),
      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 polska wersja strony wskazuje na angielską, to angielska musi bezwzględnie wskazywać z powrotem na polską, ponieważ bez takiego linku zwrotnego Google może całkowicie zignorować tę relację lub zinterpretować ją nieprawidłowo, dlatego każda wersja językowa powinna w sekcji powiązań wymieniać również samą siebie.

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

Strona z noindex nie może być indeksowanym wynikiem docelowym. Nie umieszczaj więc jej w zestawie hreflang przeznaczonym dla indeksowalnych odpowiedników.

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

Dawny raport International Targeting w Search Console został wycofany. Google nadal obsługuje hreflang, ale jego kompletność trzeba dziś weryfikować przez inspekcję URL-i, crawl serwisu i porównanie wzajemnych zestawów alternatyw.

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. Możesz użyć podfolderów (/pl/, /en/), subdomen albo osobnych domen. Podfoldery są zwykle najprostsze w utrzymaniu, ale wybór zależy od rynków, architektury i strategii serwisu.

Co jeśli mam tylko jeden język?

Nie potrzebujesz hreflang, jedynie ustaw link kanoniczny na każdej stronie. Pomaga wskazać Google preferowany wariant URL i skonsolidować sygnały wariantów z parametrami czy innym trailing slashem.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web 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 rozwija 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
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

QA w technicznym SEO oraz automatyczne testy przed deployem

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

Obsługa parametrów w URL bez duplikacji w 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