· seo / i18n / hreflang

i18n SEO pitfalls — hreflang and the slug parity trap

One slug typo silently breaks your entire hreflang graph — no build error, no browser warning. Here is what actually breaks and how to catch it before rankings drop.

By

2,553 words · 13 min read

Your hreflang implementation looks correct. All the tags are there. Lighthouse passes. Then three weeks later you notice Google is showing your English article to Vietnamese readers. There was no error message, no build failure, no HTTP 4xx response. You had a slug typo in one filename.

This is a deepdive into the two hreflang failure modes that account for most of these invisible ranking losses: the slug parity trap and canonical tag collisions. We use toolchew’s own Astro v6.3.1 codebase as the reference — it is currently correct, and we will show you exactly what would break it.

Who this is for

Developers shipping multilingual sites who have already implemented hreflang and want to understand what can silently degrade it after launch. If you have not implemented hreflang yet, start with Google’s canonical spec first — or if you are on Astro, the toolchew guide to building a multilingual Astro site covers the complete setup including hreflang and routing.

hreflang is a hint, not a directive

This distinction matters more than most developers realize.

Google’s official documentation says: “Use hreflang to tell Google about the variations of your content, so that we can understand that these pages are localized variations of the same content.”

The operative phrase is “so that we can understand” — not “so that we will display.” Google confirmed as recently as May 2025 that hreflang signals are treated as hints. Even with a perfect implementation, canonical tags, site structure, content similarity, and user context can influence which version gets shown to a given searcher.

Two related things Google does independently of your hreflang tags:

Language detection. Google does not use hreflang or the HTML lang attribute to detect page language. It uses its own content analysis. A page with English content marked hreflang="vi" will not be treated as Vietnamese — but it will have broken hreflang.

Indexing. An hreflang annotation pointing to an unindexed URL does not force Google to index that URL. If your VI articles are not indexed, hreflang pointing to them accomplishes nothing.

The three non-negotiable implementation rules

RuleRequirementWhat breaks when you skip it
Self-referencingEvery page must include itself in the hreflang setGoogle may discard the entire set
BidirectionalIf /en/ links to /vi/, /vi/ must link back to /en/Google ignores both annotations
Fully-qualified URLsUse https://example.com/... — not /en/...Tags are invalid and silently ignored

Additional requirements: language codes must use ISO 639-1 format (en, vi). Region codes are optional and must follow ISO 3166-1 Alpha-2 (en-US, vi-VN). An hreflang="x-default" tag should exist on every page to handle the case where no locale matches the user.

Why errors are invisible until they hurt you

There is no browser console warning. No build failure. No HTTP error code. The page renders, the hreflang tags appear in <head>, and Google silently ignores the broken set. GSC’s hreflang error reporting typically lags 2–4 weeks behind the actual problem. You may not find out until you are debugging a ranking drop six weeks after a content migration.

The slug parity trap

This is the failure mode that catches bilingual sites that were implemented correctly and then drifted.

Anatomy of the trap

In apps/site/src/layouts/Base.astro, toolchew’s hreflang generation looks like this:

// Base.astro — hreflang generation (Astro v6.3.1, verified 2026-05-29)
const SITE = 'https://toolchew.com';
const hreflangEnHref = articleSlug ? `${SITE}/en/${articleSlug}/` : `${SITE}/en/`;
const hreflangViHref = articleSlug ? `${SITE}/vi/${articleSlug}/` : `${SITE}/vi/`;
const hreflangXDefaultHref = articleSlug ? `${SITE}/en/${articleSlug}/` : `${SITE}/`;

And in <head>:

<link rel="canonical" href={canonicalHref} />
<link rel="alternate" href={hreflangEnHref} hreflang="en" />
<link rel="alternate" href={hreflangViHref} hreflang="vi" />
<link rel="alternate" href={hreflangXDefaultHref} hreflang="x-default" />

This code is correct. A live curl of toolchew on 2026-05-29 confirms it:

# /en/bun-vs-node/ head
<link rel="canonical" href="https://toolchew.com/en/bun-vs-node/">
<link rel="alternate" href="https://toolchew.com/en/bun-vs-node/" hreflang="en">
<link rel="alternate" href="https://toolchew.com/vi/bun-vs-node/" hreflang="vi">
<link rel="alternate" href="https://toolchew.com/en/bun-vs-node/" hreflang="x-default">

# /vi/bun-vs-node/ head
<link rel="canonical" href="https://toolchew.com/vi/bun-vs-node/">
<link rel="alternate" href="https://toolchew.com/en/bun-vs-node/" hreflang="en">
<link rel="alternate" href="https://toolchew.com/vi/bun-vs-node/" hreflang="vi">
<link rel="alternate" href="https://toolchew.com/en/bun-vs-node/" hreflang="x-default">

Self-referencing canonicals. Bidirectional hreflang. Fully-qualified absolute URLs. x-default on every page. The graph is correct.

The trap is latent. The code assumes articleSlug is identical in both en/ and vi/ content directories. That assumption holds right now. It would silently break the moment a filename drifted.

Before and after: what drift looks like

Correct state:

en/nextjs-tutorial.md    ←→  vi/nextjs-tutorial.md
/en/nextjs-tutorial/          /vi/nextjs-tutorial/
 ↑ hreflang points here        ↑ hreflang points here

After a filename typo in the VI version:

en/nextjs-tutorial.md    →   vi/nextjs-tuturial.md  ← typo in filename
/en/nextjs-tutorial/          /vi/nextjs-tuturial/   ← real VI URL
                               /vi/nextjs-tutorial/   ← what EN's hreflang emits (404)

The EN page’s hreflang still says href="https://toolchew.com/vi/nextjs-tutorial/". That URL returns a 404. Google crawls it, finds nothing, and discards the hreflang pair for the EN page. The VI page at /vi/nextjs-tuturial/ has hreflang pointing back to the EN page — which is valid — but the reciprocal is broken, so both annotations are silently ignored. Neither page gets the language-targeting benefit.

There is no build error. Astro generates both pages independently from the files that exist. The broken hreflang is emitted by the EN page’s layout logic, which only knows the EN slug and assumes the VI slug matches.

How drift happens in practice

Filename typos are the obvious cause, but there are subtler ones:

Deliberate localization of slugs. If someone decides VI articles should use Vietnamese slugs (/vi/cach-su-dung-bun/ vs /en/how-to-use-bun/), the slug-parity assumption fails by design. This is a valid localization strategy, but it requires a different hreflang architecture — each page needs to explicitly pass both slugs rather than assuming parity.

Orphan articles. A VI article is deleted or renamed, but the EN page still has hreflang="vi" pointing to the dead URL. Common after content audits.

CMS-generated slugs. If articles come from a headless CMS that generates slugs from translated titles, EN and VI slugs will naturally diverge from day one.

Detecting slug drift with curl

# Check which VI URL the EN page references
EN_PAGE=https://toolchew.com/en/nextjs-tutorial/
VI_HREF=$(curl -s "$EN_PAGE" | grep -oP 'hreflang="vi"[^>]*href="\K[^"]+')

# Verify that URL actually returns 200
curl -o /dev/null -s -w "%{http_code}" "$VI_HREF"
# 404 = slug drift confirmed

Run this across all your EN articles. If any VI URL returns a non-200 status, you have slug drift.

Canonical tag collisions

hreflang and canonical tags interact, and the interaction is destructive when misconfigured.

Google is clear on this: when a page’s canonical tag points to a different URL, Google follows the canonical and ignores the hreflang annotations on that page. The Google documentation on localized versions covers the canonical/hreflang interaction — a misconfigured canonical quietly collapses your entire hreflang graph.

Each localized page must have a self-referencing canonical — pointing to itself, not to the EN version.

The most destructive pattern

This error is often introduced by SEO plugins that treat canonical as a deduplication tool, or by developers who canonicalize all translations to the EN original:

<!-- ON /vi/bun-vs-node/ — BROKEN -->
<link rel="canonical" href="https://toolchew.com/en/bun-vs-node/"> <!-- ❌ -->
<link rel="alternate" href="https://toolchew.com/vi/bun-vs-node/" hreflang="vi">
<link rel="alternate" href="https://toolchew.com/en/bun-vs-node/" hreflang="en">

What Google does with this: follows the canonical to /en/bun-vs-node/, treats /vi/bun-vs-node/ as a duplicate of EN, drops it from the index, and ignores the hreflang annotations entirely. Vietnamese readers searching in Vietnam never see the Vietnamese version in results.

The hreflang tags are syntactically valid. Lighthouse passes. The only way to catch this is to actually inspect the canonical tag on each localized page and verify it points to the current page’s URL, not to another locale.

Why toolchew’s implementation avoids this

The canonical logic in Base.astro:

// canonicalHref defaults to the actual current URL
const canonicalHref = canonical ?? (isRoot ? `${SITE}/en/` : `${SITE}${Astro.url.pathname}`);

Astro.url.pathname always reflects the current page’s actual path — /en/bun-vs-node/ on the EN page, /vi/bun-vs-node/ on the VI page. Both pages get self-referencing canonicals by default.

The canonical prop accepts an override. That override is the risk: if any page ever passes canonical="/en/some-slug/" from a VI page context, it introduces exactly the canonical collision described above. The implementation is correct because no page currently does this — but it is a footgun that exists.

Debugging toolkit

Manual curl (do this first)

Fastest way to verify any URL’s hreflang state without installing anything:

# Inspect hreflang tags
curl -s https://toolchew.com/en/bun-vs-node/ | grep -i hreflang

# Verify the VI URL referenced actually exists
VI_HREF=$(curl -s https://toolchew.com/en/bun-vs-node/ | grep -oP 'hreflang="vi"[^>]*href="\K[^"]+')
curl -o /dev/null -s -w "VI URL status: %{http_code}\n" "$VI_HREF"

# Verify bidirectional: VI page must reference EN back
curl -s "$VI_HREF" | grep -i hreflang

Expected output from a correct implementation:

VI URL status: 200
<link rel="alternate" href="https://toolchew.com/en/bun-vs-node/" hreflang="en">
<link rel="alternate" href="https://toolchew.com/vi/bun-vs-node/" hreflang="vi">
<link rel="alternate" href="https://toolchew.com/en/bun-vs-node/" hreflang="x-default">

Screaming Frog SEO Spider

For a full site audit, Screaming Frog is the best tool available. The free tier crawls up to 500 URLs, which covers toolchew’s current scale (~300 article URLs across both locales). The paid version is €245/year for unlimited crawls.

After crawling with Crawl Analysis enabled, the Hreflang tab provides 12 filters. The three most relevant:

FilterWhat it catches
Non-200 Hreflang URLshreflang pointing to 404s, redirects, or blocked URLs — this is the slug parity trap detector
Missing Return LinksOne-way hreflang (the bidirectional requirement is broken)
Non-Canonical Return Linkshreflang target has a canonical pointing elsewhere — this catches cross-locale canonical collisions

To ensure Screaming Frog crawls both locales, go to Configuration → Include and add both toolchew.com/en/ and toolchew.com/vi/. Without this, it may only follow links from your homepage and miss one locale entirely.

Export via Bulk Export → Hreflang → All Hreflang to get every annotation across the site in a spreadsheet for spot-checking.

Screaming Frog does not have an affiliate relationship with toolchew as of this writing — this is a plain recommendation. If you are beyond the 500-URL free limit, the paid license is worth it.

Google Search Console

GSC has an International Targeting report. A note on its current state: the country targeting feature was deprecated (Google stated it “was determined to have little value for the ecosystem”). The hreflang errors tab appears to remain functional as of May 2026.

Two important limitations make GSC a secondary tool rather than a primary one:

Lag. GSC hreflang errors can take 2–4 weeks to surface after a broken deployment. By the time an error appears, the damage to indexing may already be done.

Coverage gaps. GSC reports errors Google has crawled and logged. If a problematic URL has not been crawled recently, it will not appear.

Use GSC to verify the absence of known error types, not as an exhaustive audit tool. Lead with curl + Screaming Frog for anything operational.

Lighthouse

Lighthouse has an hreflang audit that checks whether the tags are syntactically present and use valid language codes. It will not detect broken reciprocal links, slug parity failures, or canonical conflicts. Use it as a first-pass syntax check after initial implementation, then move to the tools above for ongoing health.

Reference implementation — toolchew’s Astro v6.3.1 setup

Configuration

// astro.config.mjs — toolchew production config (2026-05-29)
i18n: {
  defaultLocale: 'en',
  locales: ['en', 'vi'],
  routing: {
    prefixDefaultLocale: true,  // all locales get /en/ and /vi/ prefix
  },
},

prefixDefaultLocale: true is the right choice for a bilingual SEO site. It makes both locales structurally symmetric: /en/bun-vs-node/ and /vi/bun-vs-node/. No bare default-locale URLs at /bun-vs-node/. This symmetry makes hreflang construction mechanical: swap the locale segment, keep everything else. No ambiguity about what the canonical EN URL is.

One thing Astro v6 does not do automatically: generate hreflang tags. The i18n routing module handles URL structure. Canonical and hreflang meta tags must be added to your layout manually, which is what Base.astro does. For how Astro’s multilingual routing compares to Hugo’s approach, see Astro vs Hugo.

The sitemap gap

The @astrojs/sitemap integration supports hreflang alternates via its i18n option:

// astro.config.mjs — sitemap i18n option (not currently applied to toolchew)
sitemap({
  i18n: {
    defaultLocale: 'en',
    locales: {
      en: 'en-US',
      vi: 'vi-VN',
    },
  },
  serialize(item) { /* existing priority logic */ },
}),

toolchew’s sitemap currently does not include this option. The live sitemap at toolchew.com/sitemap-0.xml has no <xhtml:link> hreflang alternates. This is not a bug — the HTML <link> tags in <head> serve the same purpose and are what Google primarily relies on. But adding sitemap hreflang gives Google a second signal path for discovering EN/VI relationships, and the sitemap approach is actually safer for slug drift: if a VI article is missing, Astro does not generate a sitemap entry for it, rather than emitting a dangling reference to a 404 (which is what the HTML approach does).

Adding the i18n option to @astrojs/sitemap is a two-line config change. It is the next obvious improvement to toolchew’s hreflang setup.

Checklist / TL;DR

Before you deploy

  • Every localized page links to all other locale versions in the hreflang set
  • Every localized page includes a link to itself in the hreflang set
  • Every localized page has a self-referencing canonical (not pointing to EN from VI)
  • All hreflang href values use fully-qualified absolute URLs (https://..., not /vi/...)
  • Language codes use ISO 639-1 format (vi, en — not viet, not UK)
  • An hreflang="x-default" tag exists on every page
  • EN slugs and VI slugs match exactly (or your hreflang architecture uses explicit cross-locale references, not assumed parity)

After you deploy

  • Run curl -s [URL] | grep hreflang on both EN and VI versions of at least 3 articles
  • Verify the VI URL referenced in EN’s hreflang actually returns HTTP 200
  • Verify the EN URL referenced in VI’s hreflang actually returns HTTP 200
  • Run Screaming Frog on the full site; check “Non-200 Hreflang URLs” and “Missing Return Links”
  • Check GSC International Targeting → Hreflang tab for errors (may lag 2–4 weeks)

Ongoing (quarterly)

  • Audit after any content migration, CMS change, URL restructure, or batch translation
  • After adding new articles, verify the new VI counterpart file exists before deploying

Caveats

toolchew’s hreflang implementation is correct as of 2026-05-29 (verified via live curl). This article uses it as a reference, not as evidence of perfection — the latent parity risk is real and would activate the moment a VI filename drifted.

The GSC International Targeting report’s current scope is ambiguous following the country targeting deprecation. Community evidence suggests the hreflang errors tab is still active, but do not rely on it as a primary tool.

Screaming Frog, Semrush, and Ahrefs are plain recommendations. toolchew has no confirmed affiliate relationship with any of them as of this writing.

References