Part 3 of 22

Canonical URLs and hreflang: one page, one address

Duplicate content is the silent killer of multilingual and parameterized sites. Two mechanisms fix it:

Canonical tells search engines which URL is the "real" one when several serve the same content (tracking params, trailing slashes, pagination):

export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    alternates: {
      canonical: `/blog/${params.slug}`,
    },
  };
}

hreflang tells them which language versions of a page exist, so a Polish user gets the Polish page in results:

export async function generateMetadata({ params }): Promise<Metadata> {
  const { lang, slug } = params;
  return {
    alternates: {
      canonical: `/${lang}/blog/${slug}`,
      languages: {
        en: `/en/blog/${slug}`,
        pl: `/pl/blog/${slug}`,
        ru: `/ru/blog/${slug}`,
        "x-default": `/en/blog/${slug}`,
      },
    },
  };
}

The mistakes that cost real traffic:

  • hreflang must be reciprocal. If the EN page lists the PL version, the PL page must list the EN one back — otherwise Google ignores the whole group.
  • Every page must include itself in the languages map.
  • Localized slugs: if your Polish post lives at a different slug, the hreflang map has to point to the actual localized URL, not a mechanical /pl/ + same-slug guess. Generate the map from your content source, don't hardcode it.
  • x-default covers users whose language you don't serve — point it at your primary locale.
  • Canonical must point to a page that returns 200 and isn't noindex — a canonical to a redirecting URL is ignored.

Checklist:

  • Self-referencing canonical on every indexable page
  • hreflang generated from real localized slugs, reciprocal in all directions
  • x-default present
  • URL parameters (utm, filters, sorting) canonicalized to the clean URL

Want this handled for you?

I audit and fix technical SEO on Next.js sites — indexing, performance, hreflang and AI visibility.

Technical SEO service