Duplicate content can create indexing ambiguity on multilingual and parameterized sites. Two mechanisms help manage it:
Canonical identifies the preferred URL
It tells search engines which URL is preferred 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 connects language versions
It tells search engines which language versions exist and helps them select a relevant version. The lookup depends on the content model; this example assumes getLocalizedUrls() returns the real URL for each locale:
export async function generateMetadata({ params }): Promise<Metadata> {
const { lang } = params;
const localizedUrls = await getLocalizedUrls(params);
return {
alternates: {
canonical: localizedUrls[lang],
languages: {
...localizedUrls,
"x-default": localizedUrls.en,
},
},
};
}
Common implementation mistakes
- hreflang must be reciprocal. If the EN page lists the PL version, the PL page must list the EN one back — otherwise Google may disregard the 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
