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.

Four canonical configurations and their consequences A self-referencing canonical marks the German page as authoritative and it is indexed for German queries. A canonical pointing at the English page asks for the German page to be dropped. No canonical at all leaves the choice of variant to the search engine. A canonical naming a URL that redirects is discarded entirely. What each canonical choice tells a search engine You are saying What happens de → de (self) this German page is authoritative it is indexed for German queries de → en index the English page instead the German page disappears de → none no preference a variant may be chosen for you de → de with a slash a URL that redirects the signal is discarded
Only the first row expresses what a localized site actually means.

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.

The path from a wrong canonical to a lost market A crawler fetches the German page, reads a canonical pointing at the English URL, and indexes only the English page. A German reader searching in German is then offered the English page, or no result at all, because the German page was never indexed. How the wrong canonical removes a page Crawler /de/about Index German reader fetch canonical: /about index /about only searches in German the English page, or nothing
Nothing in this chain reports an error — the site simply stops appearing in one market.

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.

Five rules for canonicals on a localized site Each localized page carries a self-referencing canonical written as an absolute URL in exactly the form the page is served at. Cross-language relationships are expressed with hreflang rather than canonical. URL form is normalised before anything is annotated. Duplicate forms are redirected rather than annotated. And a per-locale assertion in continuous integration keeps it true. Getting canonicals right on a localized site 1 Every localized page is its own canonical absolute URL, exactly as served 2 Express the relationship with hreflang not with canonical 3 Normalise the URL form first one slash policy, one host, one scheme 4 Redirect duplicates rather than annotating them 301 the variants to the canonical 5 Assert it per locale in CI canonical equals the requested URL
Rule three is the one that quietly breaks the other four: a slash mismatch discards the signal.

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.

Part of Locale-Aware SEO & hreflang.