Hreflang Tag Generation

Hreflang annotations tell search engines which pages are language or regional versions of each other, so that a German searcher sees the German page and an American searcher the US English one. They are simple in principle and notoriously easy to get wrong in practice: every version must list every other version, including itself, with correct language and region codes, absolute URLs and matching canonicals. This topic covers how to generate hreflang from headless CMS data so that it is correct by construction. It belongs to Localization & SEO Optimization.

In a headless setup, hreflang depends on information spread across several systems: the CMS knows which entries are translated into which locales, the route manifest knows each version’s path, the fallback layer knows which pages are served in another language, and the canonical resolver decides each page’s preferred URL. Generating hreflang means combining those pieces consistently for every page, which is why ad-hoc implementations, a loop over supported locales that swaps the URL prefix, fail as soon as slugs are translated or some pages are not.

Where an hreflang cluster comes fromThe CMS provides the set of locales in which an entry has real content, the route manifest provides each locale's path, and the canonical resolver provides absolute canonical URLs; the cluster builder combines them, adds x-default, and emits the same cluster to the page head of every member and to the sitemap.CMStranslated localesRoute manifestpath per localeCanonicalresolverCluster builder+ x-defaultPage headevery memberSitemapxhtml:link
One cluster per entry, built once and emitted identically everywhere, is what makes annotations reciprocal.

Core Concepts

Cluster. The set of URLs that are versions of the same content in different languages or regions. Every member of a cluster must list all members, including itself.

Language and region codes. Hreflang values are an ISO 639-1 language code, optionally followed by an ISO 3166-1 alpha-2 region code: de, en-GB, pt-BR. Region alone is invalid, and common mistakes such as en-UK or jp are silently ignored.

x-default. A special value for the URL that serves users whose language matches none of the others, usually a language selector or the default locale’s page.

Reciprocity. If page A lists page B, page B must list page A. Search engines ignore one-sided annotations, and a single wrong URL can invalidate the relationship.

Self-reference and canonicals. Each member lists itself and has a canonical pointing to itself. A member whose canonical points elsewhere is not treated as a valid version.

Integration Contract

Hreflang is only as good as the data behind it. The contract has four parts. Membership: a page belongs to an hreflang set only if it has genuine content in its locale; fallback pages, drafts and noindex pages are excluded. URLs: every URL comes from the route manifest, absolute and in canonical form, identical to the page’s canonical tag. Codes: each site locale maps to exactly one hreflang code in configuration, reviewed for correctness. Delivery: hreflang sets are emitted either in page heads, in sitemaps, or in both, generated from the same function.

Bash
# .env: hreflang configuration
HREFLANG_CODES="en:en,en-GB:en-GB,de:de,fr:fr,fr-CA:fr-CA,pt-BR:pt-BR,ja:ja"
HREFLANG_X_DEFAULT=/en/            # or a language selector path
HREFLANG_DELIVERY=head+sitemap
Common hreflang mistakes and their causesFrequent hreflang errors in headless sites, how they appear in search console reports and the generation step that prevents each.MistakeReported asPrevented byMissing return linksno return tagsone cluster emitted to every memberInvalid codes (en-UK, jp)unknown language codecode map in configurationFallback pages in clusteralternate with other canonicalmembership from real contentRelative or wrong URLsURL not foundURLs from manifest + canonical resolverGuessed paths with translated slugs404 alternatespaths from manifest
Nearly every error comes from building clusters in more than one place or from the wrong data.

Building Clusters

The builder takes an entry, asks the CMS layer which locales have genuine content, looks up each locale’s path in the manifest, and maps locales to hreflang codes. It returns the same list for every member.

TypeScript
// lib/seo/hreflang.ts
import { manifestPath } from "@/lib/routes/manifest";
import { translatedLocales } from "@/lib/cms/translations";

const CODES: Record<string, string> = { en: "en", "en-GB": "en-GB", de: "de", fr: "fr", "fr-CA": "fr-CA", "pt-BR": "pt-BR", ja: "ja" };
const ORIGIN = process.env.SITE_ORIGIN!;
const X_DEFAULT_LOCALE = "en";

export interface Alternate { hreflang: string; href: string }

export async function hreflangCluster(entryId: string): Promise<Alternate[]> {
  const locales = await translatedLocales(entryId);            // locales with real, published content
  const alternates: Alternate[] = [];
  for (const locale of locales) {
    const path = await manifestPath(entryId, locale);
    const code = CODES[locale];
    if (path && code) alternates.push({ hreflang: code, href: `${ORIGIN}${path}` });
  }
  if (alternates.length < 2) return [];                        // a single version needs no cluster
  const xDefault = alternates.find((a) => a.hreflang === CODES[X_DEFAULT_LOCALE]);
  if (xDefault) alternates.push({ hreflang: "x-default", href: xDefault.href });
  return alternates.sort((a, b) => a.hreflang.localeCompare(b.hreflang));
}

The page’s metadata calls the builder and emits the hreflang set only when the current page is itself a member, never on fallback pages. The sitemap generator calls the same builder for every entry. Because both use one function and the same data, the head and the sitemap can never disagree.

Regional Variants

Regional variants, such as en-US and en-GB or es-ES and es-MX, are where hreflang earns its keep: the pages may be nearly identical, and without annotations search engines may treat them as duplicates and show the wrong one. Give each regional variant its own code, and add a language-only code for the version that should serve other regions of that language. For example, en-GB for the UK site, en-US for the US site, and en pointing at the US site for English speakers elsewhere. Region codes must be valid ISO country codes; en-UK is a frequent and silent mistake, since the correct code is en-GB. The regional variants guide covers the patterns in detail.

Hreflang and Fallback Pages

Fallback pages are the most frequent source of hreflang confusion. A German URL that serves English content because the German translation does not exist yet is not a German version of the page. Including it in the cluster tells search engines that /de/pricing is German content, which contradicts both its actual language and its canonical, which should point to the English source. The result is either ignored annotations or English content ranking in German results. Exclude fallback URLs from clusters entirely, as described in canonicalizing fallback pages, and let the cluster grow automatically when the translation is published. The same applies to pages marked noindex and to pages whose canonical points elsewhere for any other reason: a cluster may only contain indexable, self-canonical URLs.

Multiple Domains and Sites

Hreflang sets can span domains, for example example.de, example.fr and example.com/en, and even separately deployed sites. Cross-domain hreflang sets follow the same rules, with two extra requirements. Every site must emit the complete hreflang set, so a builder that only knows its own site’s pages is not enough; the route manifest or a shared hreflang set service must cover all sites. And each domain must be verified in the search engine’s tools if hreflang sets are delivered through sitemaps that list URLs on other hosts. Multi-tenant platforms, where tenants are separate brands rather than language versions, must never pages in the set across tenants, even when their content looks similar; hreflang sets are for translations of the same content, not for related sites.

Head Tags or Sitemaps

Hreflang can be delivered as link elements in each page’s head, as xhtml:link entries in XML sitemaps, or as HTTP headers for non-HTML files. Head tags are visible when inspecting a page and are processed when the page is crawled. Sitemap annotations keep HTML smaller for sites with many locales and let clusters be updated without re-rendering pages, but they are only processed when the sitemap is read. Either is fine alone; both together are fine as long as they come from the same builder. What must never happen is two independent implementations that drift, which is the most common cause of contradictory annotations in audits.

Language Switchers and Hreflang Together

A page’s language switcher and its hreflang cluster describe the same thing, the page’s other language versions, to different audiences. Build them from the same cluster. The switcher then links each available language to the right translated page, with hreflang attributes on the links and each language named in its own language, and for languages without a version of the page it links to that locale’s homepage or omits the language. Readers get switcher links that never lead to 404s or fallbacks they did not expect, and every link in the switcher is also a crawlable signal that reinforces the annotations. When the switcher and the cluster come from different code, they drift in the same way head tags and sitemaps do, and readers find the discrepancy long before audits do.

Caching & Invalidation

Hreflang sets change when translations are published, unpublished or have their slugs changed. Each such event affects every member of the hreflang set, because every member lists the others. Tag cached pages with an hreflang set tag, such as hreflang set:{entryId}, and revalidate it whenever any locale of the entry changes, together with the sitemap chunks that contain the entry. Without this, the newly published German page lists all its siblings, but the siblings keep serving cached heads without the German URL, and search engines report missing return links until the caches expire.

Preview & Draft Handling

Draft translations must never appear in published clusters. Build clusters from published content only, even when rendering a published page while other locales have drafts. In preview, it helps editors to see what the cluster will look like after publishing the current draft, so the preview route can build a hypothetical cluster that includes the draft locale, clearly marked as such, without affecting production caches.

Error Handling & Resilience

Build clusters defensively. If the manifest has no path for a locale that the CMS reports as translated, leave that locale out and log the inconsistency, rather than emitting a guessed URL that 404s. If the CMS request fails, render the page without hreflang rather than with a partial cluster, and do not cache that response for long. An incomplete cluster emitted by some members and not others is worse than no cluster, because it produces reciprocity errors across the whole set.

Testing & Observability

Test the builder with fixtures for entries translated into all, some and one locale, with translated slugs and with regional variants, asserting exact output. In CI, render all members of a sample cluster and assert that their emitted clusters are identical, which is the reciprocity property expressed as a test. After deploys, crawl a sample of clusters in production and compare, and run the sitemap validation described in debugging hreflang errors. Monitor the international targeting reports in search console per locale; a sudden rise in errors almost always follows a change to routing, slugs or caching.

Hreflang errors after moving to a single cluster builderHreflang errors reported in search console per month, before and after replacing separate head and sitemap implementations with one cluster builder fed by the route manifest.Separate implementations14800 errors reportedMonth 1, single builder2100 errors reportedMonth 3, single builder90 errors reported
Most errors disappeared once head and sitemap stopped disagreeing.

Worked Example

A travel company with nine locales, including three English and two Spanish regional variants, generated hreflang in its page template by looping over all supported locales and swapping the prefix, and separately in a sitemap plugin that listed only the default locale’s alternates. After translated slugs were introduced, half the alternates pointed at non-existent paths, and search console reported nearly fifteen thousand errors. The team replaced both with one builder fed by the route manifest and the CMS’s translation data, fixed en-UK to en-GB, excluded fallback pages, and added cluster tags for revalidation. Errors fell by more than 99 percent over three months, and regional English pages began appearing in the correct countries’ results.

Migrating an Existing Implementation

Most sites already emit some hreflang, and replacing it is a migration worth planning. Start by crawling the current annotations and search console reports to understand the baseline, per locale. Build the new cluster builder alongside the old code and, in CI or a staging environment, compare the two outputs for a large sample of pages; every difference is either a bug being fixed or a regression being introduced, and both deserve a look. Switch head tags and sitemap annotations in the same release, so they never disagree in production, and revalidate all pages and sitemaps at once rather than letting caches expire gradually. Expect search console reports to lag by weeks; judge the result by crawling your own site, not by waiting for the reports to settle.

Performance of Cluster Lookups

Building a cluster needs the list of translated locales and a path per locale, for every page render and every sitemap entry. Keep both lookups cheap. The route manifest already answers path lookups from memory or an edge store. The translated-locale list can be stored in the manifest too, as a field on each entry, updated by the same webhooks, so the builder needs no CMS request at all. Cache built clusters per entry for the duration of a render, since the page head, JSON-LD and language switcher all need the same information. Done this way, hreflang adds nothing measurable to render time even for sites with a dozen locales.

Adding and Removing Locales

Locale changes ripple through every cluster. When a new locale launches, each page translated into it joins its cluster, and every existing member must be revalidated to list the new URL; a site-wide revalidation at launch time, together with regenerated sitemaps, is simpler than tracking individual pages. When a locale is retired, its URLs should redirect to the closest remaining locale, and they must leave every cluster in the same release, or the remaining pages keep pointing at redirects, which search engines report as errors. Keep the code map in configuration so both operations are a reviewed configuration change plus a revalidation, not a code change scattered across templates.

Ownership

Hreflang crosses team boundaries, so give it an owner. The engineering team owns the builder, codes configuration, caching and tests. SEO specialists own the decisions: which regional variants exist, what x-default points at, whether hreflang goes in heads, sitemaps or both, and the review of search console reports. Market teams own the correctness of their locale’s content and slugs. A short monthly review of hreflang errors per locale, with the engineering owner present, catches regressions while they are still small. Record decisions such as the choice of x-default in the configuration file’s comments, so the reasoning survives team changes and nobody reverses a deliberate choice by accident.

Frequently Asked Questions

Do we need hreflang if every locale has its own domain?

Yes, when the domains serve versions of the same content. Hreflang works across domains, as long as every member lists every other member.

Does hreflang affect ranking?

It does not boost rankings; it helps search engines show the right version to the right users, which avoids duplicates competing with each other and improves click-through in each market, because readers see results in their own language and for their own region.

Should pages with only one language have hreflang?

No. A cluster needs at least two versions; a lone self-reference adds nothing and only makes the page head longer.

What about pages that are translated only partially?

If the page is genuinely localized, with only a few fields falling back, include it. If the whole page falls back, exclude it.

Is hreflang needed for pages behind a login?

No. Pages that are not indexable, such as account areas, need no annotations. Focus on public, indexable pages.

Do other search engines use hreflang?

Google and Yandex use hreflang annotations; Bing relies more on the content-language meta tag and lang attributes. Setting lang correctly on every page covers both approaches.

Should hreflang include query parameters?

No. Use the canonical URLs of each version, without tracking or filter parameters, exactly as they appear in canonical tags and sitemaps.

How many locales can a cluster have?

There is no strict limit, but large clusters make HTML heavy. Sites with many locales often move annotations to sitemaps to keep page heads small and rendering fast.