Concepts & Strategy6 min read

How to Ensure Hreflang Is Correct When Using a Headless CMS

A single mistyped locale code, "en-UK" instead of the valid "en-GB", is enough to make Google ignore your hreflang annotations entirely, and you often will not find out until organic traffic in a whole region quietly craters.

Published August 25, 2026

A single mistyped locale code, "en-UK" instead of the valid "en-GB", is enough to make Google ignore your hreflang annotations entirely, and you often will not find out until organic traffic in a whole region quietly craters. On a headless stack the failure mode is worse, because the URLs, the locale metadata, and the rendered `<link rel="alternate" hreflang>` tags usually live in three different systems that no one owns end to end. The CMS holds translations, the frontend framework builds routes, and the tags get assembled at render time from whatever data happens to be available, so a missing translation or a broken reference produces silently invalid markup.

This is fundamentally a content modeling problem, not a templating problem, and that reframe is the point of this guide. Sanity, the Content Operating System for the AI era, treats locale relationships as first-class, queryable data in the Content Lake rather than something you reconstruct at build time. When your translations, their language codes, and their canonical URLs are structured and joinable in one query, generating correct, reciprocal, self-referencing hreflang tags becomes deterministic instead of hopeful.

We will cover the rules that actually break sites, how to model locale relationships so the data cannot drift, and how to query and render tags that stay reciprocal as content changes.

The hreflang rules that actually break headless sites

Hreflang has a small rulebook, and violating any one line silently disables the whole annotation set for that page. First, every value must be a valid ISO 639-1 language code, optionally paired with an ISO 3166-1 Alpha 2 region code. "en-GB" is valid; "en-UK" is not, and Google discards the entire cluster rather than the single bad line. Second, annotations must be reciprocal: if the English page points to the French page, the French page must point back. A one-way reference is treated as untrusted and ignored. Third, every page in a cluster must reference itself. Fourth, each URL must be a fully qualified, canonical, absolute URL, not a relative path and not a redirecting or parameterized variant.

On a traditional monolith these rules are annoying but centralized. On a headless stack they fracture across systems. The language code often lives as an editorial field in the CMS, the URL is computed by the frontend router, and the reciprocal links depend on every sibling translation existing and being published at the moment of render. A translator unpublishes the German article, the English page still lists a German alternate pointing at a now-404 URL, and the cluster degrades. Nobody wrote a bug; the data simply drifted out from under the template.

The lesson enterprise teams learn the expensive way is that hreflang correctness is a property of your content model, not your rendering layer. If the locale relationships, the codes, and the canonical URLs are not stored as structured, validated, joinable data, then every render is a fresh opportunity to emit invalid markup. The rest of this guide treats the model as the fix.

Model locale relationships as data, not as folder conventions

The most common headless anti-pattern is to encode locale in the URL or the file path and hope the tags fall out of that convention. A route like `/de/produkte/schuhe` implies a German page, but the convention carries no guarantee that a French sibling exists, that its code is valid, or that it points back. Conventions are not constraints, and hreflang needs constraints.

Sanity's approach is to model the relationship explicitly. There are two established patterns. In field-level translation, a single document holds all locales as internationalized fields, so the sibling set is intrinsic to the document and never drifts. In document-level translation, each locale is its own document and a translation metadata document joins them, which suits long-form content that diverges per market. In both cases the language code is a real, validated field on the document, not an inference from a path. You define these schemas as portable `defineType` objects in `sanity.config.ts`, add a validation rule that rejects anything not on your approved locale list, and the Studio enforces it at authoring time. TypeGen then emits TypeScript types for those fields, so the frontend consumes the locale data with compile-time safety rather than string guesswork.

Because the relationship is a first-class reference in the Content Lake, you can ask a single question at query time: for this document, what are all its published translations, what is each one's validated language code, and what is each one's slug? That is exactly the shape hreflang generation needs, and it comes back joined in one round trip. Modeling the relationship as data is what makes the reciprocal, self-referencing rules mechanically enforceable instead of aspirational.

Generate the full cluster in one GROQ query

Once locale relationships live in the Content Lake as references, GROQ lets you retrieve the entire hreflang cluster for a page in a single round trip, which is the difference between deterministic tags and hopeful ones. GROQ is projection-first: you describe the exact shape you need, follow references with the dereference operator `->`, and filter to only published documents, all in one query. You never fetch a document and then fire N follow-up requests to reconstruct its siblings; the projection returns the language code and slug for every translation alongside the page itself.

That single-query property matters for correctness, not just performance. Because the reciprocal set is computed from the same source of truth for every locale in the cluster, the English page and the French page derive their alternates from identical data. There is no window where one page has been rebuilt with fresh translation data and its sibling has not. When you also filter the projection to translations that are actually published, a translation that gets unpublished simply drops out of every sibling's cluster on the next query, so you never emit an alternate pointing at a dead URL.

For teams that want changes to propagate without a full rebuild, the Live Content API delivers query results in real time, so an editorial change to a translation's status or slug can flow to the rendered cluster as it happens. The combination, structured references plus a projection that returns the whole cluster plus live delivery, is what keeps reciprocity intact as content churns. The query is the same three lines whether a document has two translations or twenty.

Render self-referencing, reciprocal tags without drift

With the cluster data in hand, rendering is mechanical, and it should stay that way. For each page you emit one `<link rel="alternate" hreflang="...">` per translation in the cluster, including a self-referencing tag for the current page and, when appropriate, an `x-default` entry pointing at your language-selection or primary-market page. The rule that trips people up is that this exact same complete set must appear on every page in the cluster, self-reference included. If you generate the list by iterating over the sibling array returned by your query and always add the current page to that array, self-reference and reciprocity fall out for free.

The absolute-URL requirement is where headless renders quietly go wrong. The query returns a slug and a language code; the frontend must compose the fully qualified, canonical URL from those, using the same host and path logic that produces the page's own canonical tag. If your canonical composition and your hreflang composition diverge, you get alternates that point at redirecting or non-canonical variants, which Google distrusts. Keep one URL-building function and call it from both places.

Because the tags are derived from the model rather than hand-authored, Visual Editing and the Presentation Tool let editors see the live rendered page, including its head, wired back to the exact fields that drive it. An editor changing a translation's slug can see the downstream effect instead of discovering it through a traffic report. The tags are an output of governed content, not a separate artifact someone maintains by hand.

Govern translation workflows so clusters stay whole

Most hreflang regressions are not code bugs; they are workflow gaps. A market goes live before its translation is ready, or a locale is retired and its alternates linger, or two translations of the same page ship days apart and the cluster is incomplete in between. If publishing is per-document and uncoordinated, the reciprocal set is broken for exactly as long as the locales are out of sync, which on a large site can be most of the time.

This is where treating content operations as a governed system rather than a pile of documents pays off. Content Releases let a team stage a set of changes, a new locale, a batch of translated pages, a slug change and its downstream effects, and publish them together as a coordinated unit, so a cluster does not go live half-formed. Scheduling extends the same guarantee to timed launches. Roles & Permissions constrain who can publish into a market, and Audit logs give you the record of who changed a locale relationship and when, which matters when a region's traffic moves and someone has to trace why.

Modeling and rendering get you correct tags at a moment in time; governance keeps them correct across the editorial churn of a real multi-market operation. The point of a Content Operating System, as opposed to a headless CMS that stops at storing and serving fields, is that the workflow rules live next to the content, so the constraint that translations ship as a complete cluster is enforced by the platform rather than by a runbook someone hopes the team reads.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.