The English page declares /fr/about as its French equivalent via hreflang. The French page canonicals back to /en/about. Google sees one signal saying "these are alternates" and another saying "the French page is a duplicate of the English one". Canonical wins; hreflang is ignored. The cluster fails silently — French users see the English page in search, and the translated content never gets indexed. This guide covers the right canonical pattern for translated pages and the audit to find broken clusters.
<!-- /en/about (English) --> <link rel="canonical" href="https://example.com/en/about" /> <link rel="alternate" hreflang="en" href="https://example.com/en/about" /> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about" /> <!-- /fr/about (French) — BROKEN --> <link rel="canonical" href="https://example.com/en/about" /> <!-- BAD: canonicals to English --> <link rel="alternate" hreflang="en" href="https://example.com/en/about" /> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about" />
Google sees the French page declaring itself as a duplicate of the English. Drops it from the index. Hreflang annotation void. French users get the English page.
<!-- /en/about --> <link rel="canonical" href="https://example.com/en/about" /> <link rel="alternate" hreflang="en" href="https://example.com/en/about" /> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about" /> <!-- /fr/about --> <link rel="canonical" href="https://example.com/fr/about" /> <!-- self-canonical --> <link rel="alternate" hreflang="en" href="https://example.com/en/about" /> <link rel="alternate" hreflang="fr" href="https://example.com/fr/about" />
Each variant tells Google "I'm the canonical version of this locale's content". Hreflang connects them as locale alternates. Google indexes both, serves each to the right audience.
The cross-locale canonical usually comes from one of these mistakes:
# Check canonical on each variant for url in https://example.com/en/about https://example.com/fr/about https://example.com/de/about; do echo "=== $url ===" curl -s "$url" | grep -oE '<link[^>]*rel="canonical"[^>]*>' done # Each canonical href should equal the URL it's served from
// Confirm WPML self-canonicals translated posts // WPML → SEO → "Set hreflang and canonical tags" // - Mode: "Same URL per language" → each gets self-canonical // - Avoid "Use default language URL" if duplicate handling
// Polylang handles self-canonicals correctly with Yoast/Rank Math // If issues, check that: // - Each translated post has Yoast canonical set to its OWN permalink (not source) // - Yoast → Translation tab → confirm canonical settings
// app/[locale]/about/page.tsx
export async function generateMetadata({ params }) {
const url = `https://example.com/${params.locale}/about`;
return {
alternates: {
canonical: url, // self-canonical — uses the served locale
languages: {
'en': 'https://example.com/en/about',
'fr': 'https://example.com/fr/about',
'de': 'https://example.com/de/about',
'x-default': 'https://example.com/en/about'
}
}
};
}
---
// pages/[locale]/about.astro
const { locale } = Astro.params;
const canonical = `https://example.com/${locale}/about`;
---
<link rel="canonical" href={canonical} />
<!-- hreflang as before -->
// In a partial/component
function getCanonical(currentLocale, slug) {
return `https://example.com/${currentLocale}/${slug}`;
}
<link rel="canonical" href="${getCanonical('fr', 'about')}" />
// NEVER pick a default-locale URL here
<!-- en.example.com/about --> <link rel="canonical" href="https://en.example.com/about" /> <!-- fr.example.com/about --> <link rel="canonical" href="https://fr.example.com/about" />
Same pattern — each subdomain self-canonicals. Cross-subdomain canonicals also void hreflang.
<!-- example.co.uk/about --> <link rel="canonical" href="https://example.co.uk/about" /> <!-- example.fr/about --> <link rel="canonical" href="https://example.fr/about" />
Each ccTLD canonicals within its own domain. Hreflang declares the cross-ccTLD alternates.
site:example.com/fr/ now returns the French variants in results. Before the fix, only English versions appeared.