Canonical pointing to the default locale
Traffic from Germany falls to nothing over a few weeks. The German pages are live, translated and reachable; a site search shows them; the sitemap lists them. Search results for German queries show the English page instead, or nothing at all.
Somewhere in a shared layout, every localized page is emitting a canonical link that points at the default locale’s URL. That single line asks search engines to drop every translated page from the index, and they comply.
Root cause: canonical is for duplicates, and translations are not duplicates
The canonical link exists to resolve one piece of content reachable at several addresses — with and without a tracking parameter, with and without a trailing slash, at a legacy path and a current one. It says: these are the same thing, index this one.
Translations look superficially like that case. The German page has the same structure, the same images and the same purpose as the English one, and a developer reaching for a way to say “these belong together” finds canonical first because it is the annotation everyone already knows.
But a translation is not the same content — it is different content for a different audience, and a canonical annotation is a strong, obeyed instruction. Pointing it across languages asks for the localized page to be discarded, and it works exactly as asked.
The relationship that does need expressing has its own annotation: hreflang, which says “these are alternatives for different audiences, index all of them and serve the right one”. Using canonical for that job is the most damaging configuration mistake on a localized site, and it is silent — nothing errors, nothing warns, and the effect arrives weeks later as a traffic decline nobody can attribute.
Minimal reproducible example
// A shared layout computing one canonical for every page in every language.
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
{/* Wrong: every locale points at the English URL */}
<link rel="canonical" href={`https://example.com${usePathname().replace(/^\/[a-z]{2}/, '')}`} />
</head>
<body>{children}</body>
</html>
);
}
The regular expression strips the locale prefix, which is precisely the information that made the URL distinct. Every localized page now claims the English page is authoritative.
The fix: self-referencing canonicals, relationships in hreflang
import { LOCALES, ORIGIN } from '@/i18n/config';
export function localeHead(locale: string, pathWithoutLocale: string) {
const self = `${ORIGIN}/${locale}${pathWithoutLocale}`;
return {
canonical: self, // points at itself
languages: Object.fromEntries([
...LOCALES.map((l) => [l.language, `${ORIGIN}/${l.code}${pathWithoutLocale}`]),
['x-default', `${ORIGIN}/${LOCALES[0].code}${pathWithoutLocale}`],
]),
};
}
The canonical names the URL being served, character for character. The languages map carries the cross-language relationship, and because it is generated from the locale list it stays reciprocal as locales are added — the property described in locale-aware SEO and hreflang.
The near-miss versions
Two configurations look correct and are not, and both are more common than the outright cross-language canonical.
A canonical that differs only in URL form. The page is served at /de/about and canonicalises to /de/about/, or the site is served from www and the canonical omits it. String equality is what matters here: a canonical that does not exactly match a URL the site serves is a canonical pointing at a redirect, and a canonical pointing at a redirect is discarded. The fix is to normalise before generating — decide a slash policy and a host once, and derive every URL from that decision.
A canonical generated from the request rather than from the route. Building the canonical from the incoming URL rather than from the resolved route means every query parameter, every tracking tag and every casing variation becomes its own canonical, which defeats the entire purpose. The canonical must come from the route the application resolved, not from what the client happened to type.
Both failures share a signature in reporting tools: the page is described as an alternative to a canonical you did not intend, or as excluded by a canonical that is not the one you can see in the markup. When those descriptions disagree with the page source, the tool is reporting a URL form and the source is showing another.
Where the wrong canonical usually comes from
Knowing the shapes that produce this bug makes it findable in a codebase without reading every template.
The shared layout is the most common source. One component renders the head for every page in every language, and the canonical is computed there from a path that has already had the locale removed — often because the same helper is used for analytics grouping, where stripping the locale is exactly right.
The content management system is the second. A canonical field on a content entry is authored once, in the source language, and inherited by every translation of that entry. Every localized page then emits the source language’s URL, and no code in the repository is responsible.
The migration artifact is the third. A site that added localization to an existing single-language application often kept a canonical helper written when there was only one URL per page. It was correct then and is wrong now, and because it predates the localization work nobody thinks to look at it.
A single search finds all three: grep the repository for rel="canonical" and for whatever helper generates it, and check each result against the rule that the output must equal the URL being served. In a typical application there are one or two such places, which is what makes this a ten-minute audit rather than a project.
The same audit is worth repeating after any change to routing, because a canonical that was correct under one URL shape is not automatically correct under another — adding a locale prefix, changing a slash policy, or moving to a new domain each invalidate it.
Verification
# Every localized page must be its own canonical, byte for byte
for p in "" de/ fr/ ar/; do
url="https://example.com/${p}about"
got=$(curl -s "$url" | grep -o '<link rel="canonical"[^>]*>' | grep -o 'href="[^"]*"')
echo "$url -> $got"
done
# Expected — each line names the URL it was fetched from
# https://example.com/about -> href="https://example.com/about"
# https://example.com/de/about -> href="https://example.com/de/about"
Turn that loop into a CI assertion across a sample of routes and every locale. It is a cheap check that catches the whole class, including the near-miss forms, because it compares the canonical against the URL actually requested rather than against an expectation written by the same person who wrote the bug.
When to escalate
If canonicals are correct and localized pages are still not indexed, look at the annotations next. A discarded hreflang set leaves each page indexable but unassociated, so search engines may serve the wrong language even though every page exists — the return-tag failures covered in locale-aware SEO and hreflang.
If the pages are indexed but not ranking in the target market, the issue is no longer structural. Thin or machine-generated translations, or a page whose content genuinely does not match what that market searches for, are content problems that no annotation fixes.
If a page was dropped and has now been corrected, recovery takes time and is uneven — heavily crawled pages return within days, rarely visited pages within weeks. Requesting reindexing for the most important URLs is worth doing; requesting it for everything is not.
FAQ
Is it ever right to canonicalise across locales?
Only when the pages are genuinely identical, which in practice means untranslated. If /de/about renders the same English text as /about because it was never translated, then it is a duplicate and canonicalising it is honest. The better fix is to stop serving an untranslated page at a localized URL at all.
What about regional variants of the same language?
If de-DE and de-AT serve identical content, do not create two URLs. If they serve different content — different pricing, different legal wording — each is its own canonical and both appear in the annotation set with regional tags. What fails is creating two URLs with identical content and annotating them as regional variants.
Does a canonical override hreflang, or the other way round?
They answer different questions and are evaluated together, but a canonical pointing away from the page effectively removes it from consideration, so a bad canonical defeats a good annotation set. Get the canonical right first; the annotations only matter for pages that are indexed.
Should the canonical include query parameters?
Almost never. Parameters that do not change the content — tracking, session, campaign tags — must not appear, or every share produces a new canonical. Parameters that genuinely select different content are arguably a different page and are usually better expressed as a path.
Related
- Locale-Aware SEO & hreflang — the annotation that expresses what canonical should not.
- Nuxt i18n Module Setup — a module that emits self-referencing canonicals automatically.
- Next.js i18n Routing Setup — where
generateMetadataproduces the same output. - SvelteKit route prefixing for multiple locales — canonical and hreflang emission without a module.
Part of Locale-Aware SEO & hreflang.