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
languagesmap. - 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-defaultcovers 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-defaultpresent - URL parameters (utm, filters, sorting) canonicalized to the clean URL