hreflang x-default for Headless Locale Selectors

The x-default annotation answers a single question — where should someone go when none of your locales match them — and in a decoupled build it is usually answered by an IP-based redirect that Googlebot never experiences.

When to Use This Approach

Set or fix x-default when:

  • The site serves three or more locales and traffic arrives from countries none of them target.
  • Search Console’s international targeting report shows alternate pages without return tags.
  • A geolocation redirect sits in front of the locale URLs, which is the usual root cause.

What the Fallback Is Actually For

Every alternate says “this URL is for these speakers in this country.” The default says “and everyone else goes here.” Without it, everyone else is assigned by inference.

Matched locales and the unmatched fallback Three matched visitor groups routed to their locale alternates and a fourth unmatched group routed to the x-default URL, which is also a real indexable page. visitor signal URL served de-DE fr-FR en-GB anything else /de/produkte/ /fr/produits/ /uk/products/ /products/ — the x-default target The default must be a real page that returns 200 to a crawler from any country

Implementation Steps

Step 1: Decide what the fallback should be

Two viable choices:

  a) The most broadly applicable locale (usually international English at /)
     -> best when one locale genuinely serves an unmatched visitor well

  b) A language selector page with real content
     -> only when you have distinct regional offerings and the selector
        page itself is useful, not an empty list of flags

Validation: Ask what a visitor from an unlisted country actually needs. If the answer is “the English version”, option (a) is correct and simpler.

Step 2: Make the default target directly crawlable

This is where most implementations fail: the x-default URL exists but redirects by IP, so a crawler never sees it.

// middleware.js — never redirect a crawler or the default URL by geolocation
const LOCALE_PREFIXES = new Set(['de', 'fr', 'uk', 'jp']);

export function middleware(request) {
  const { pathname } = new URL(request.url);
  const first = pathname.split('/')[1];

  // A request that already names a locale is served as-is.
  if (LOCALE_PREFIXES.has(first)) return NextResponse.next();

  // The default path is served as-is too — no geolocation redirect.
  return NextResponse.next();
}

SEO impact: Every alternate, including the default, returns 200 to any requester, which is the precondition for the whole annotation set being accepted.

Validation: curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://example.com/products/ returns 200 with no redirect target.

Step 3: Generate the full alternate set, including the default

Every page in a locale group must list every alternate and the default. A partial set is treated as no set.

// lib/alternates.js — one source of truth for the whole group
export const LOCALES = [
  { code: 'en-GB', prefix: '/uk' },
  { code: 'de-DE', prefix: '/de' },
  { code: 'fr-FR', prefix: '/fr' },
  { code: 'ja-JP', prefix: '/jp' },
];

export const DEFAULT_PREFIX = '';   // the root, international English

export function alternatesFor(entry) {
  const languages = Object.fromEntries(
    LOCALES.map((l) => [l.code, `https://example.com${l.prefix}/${entry.paths[l.code]}/`]),
  );
  languages['x-default'] = `https://example.com${DEFAULT_PREFIX}/${entry.paths.default}/`;
  return languages;
}
// app/[[...locale]]/products/[slug]/page.jsx
export async function generateMetadata({ params }) {
  const entry = await cms.products.getGroup(params.slug);
  return { alternates: { languages: alternatesFor(entry) } };
}

SEO impact: Every URL in the group declares the complete set, which is what makes the return-tag requirement satisfiable.

Validation: curl -s <any locale url> | grep -c 'hreflang=' returns the locale count plus one, on every alternate.

Step 4: Replace geolocation redirects with a banner

A suggestion preserves the visitor’s choice and the crawler’s access at once.

// components/LocaleSuggestion.jsx — suggest, never redirect
export function LocaleSuggestion({ currentLocale, suggested }) {
  if (!suggested || suggested === currentLocale) return null;
  return (
    <aside role="note" className="locale-suggestion">
      <p>This page is also available in {suggested.label}.</p>
      <a href={suggested.href} hrefLang={suggested.code}>
        Switch to {suggested.label}
      </a>
    </aside>
  );
}

SEO impact: Googlebot crawling from any location reaches the URL it requested, so every alternate is fetched and every annotation can be confirmed.

Validation: Request each locale URL with a geographically varied proxy and confirm all return 200 without a Location header.

Step 5: Verify the annotations resolve both ways

#!/usr/bin/env bash
# ci/hreflang-check.sh — every alternate must return 200 and name the group
set -euo pipefail
url="$1"
alts=$(curl -s "$url" | grep -oP '(?<=hreflang=")[^"]+" href="[^"]+' \
  | sed 's/" href="/\t/')
echo "$alts" | while IFS=$'\t' read -r lang href; do
  code=$(curl -s -o /dev/null -w '%{http_code}' "$href")
  back=$(curl -s "$href" | grep -c "href=\"$url\"")
  printf '%-10s %s %s return-tag=%s\n' "$lang" "$code" "$href" "$back"
done

Validation: Every row shows 200 and a non-zero return-tag count. Any row with a 3xx is a geolocation redirect still in place.

Why Redirects Break the Whole Set

The failure is not that one annotation is wrong; it is that the crawler never reaches the pages that would confirm the others.

Crawl outcome with and without geolocation redirects With redirects, every crawler request collapses onto one locale and the other alternates are never fetched; without redirects each locale is fetched and its return tags confirmed. with IP redirects without them /uk/ /de/ /fr/ /jp/ only /uk/ fetched 3 alternates unconfirmed /uk/ /de/ /fr/ /jp/ 200, return tag ok 200, return tag ok 200, return tag ok 200, return tag ok Googlebot crawls from a small set of locations — a redirect by IP collapses the whole group

SEO Impact Summary

The unmatched population is larger than most teams expect, which is why the fallback choice is worth making deliberately rather than letting Google infer it.

How much traffic the fallback actually governs A bar of total international traffic split into matched locales and an unmatched share that the x-default annotation governs. International sessions by locale match matched exactly matched by language unmatched — governed by x-default without the annotation, assigned arbitrarily The fallback is not an edge case on a site with more than a few markets
Signal What improves What breaks if misconfigured
Locale targeting Unmatched visitors land on a deliberate fallback rather than an inferred one An x-default pointing at a redirecting URL is ignored entirely
Alternate recognition Every locale is fetched, so return tags confirm and the group is accepted Geolocation redirects leave most alternates uncrawled
Duplicate control Locale variants are recognised as alternates rather than duplicates An unrecognised group means locales compete with each other
Crawl efficiency No redirect hops between locale URLs Every locale request costing a redirect wastes budget at locale scale

Measurable signals to watch:

  • Count of alternates returning 200 to an out-of-region request, which should be all of them.
  • International targeting errors in Search Console, specifically missing return tags.
  • Impressions per locale, where a single dominant locale despite multi-market content suggests the group is not recognised.

Edge Cases and Gotchas

x-default must appear in every page’s set, not only on the home page A group is a per-page relationship. Declaring the default only at the root leaves every deeper page without a fallback.

Language-only codes are usually the right level de rather than de-DE unless you genuinely serve different content to Germany and Austria. Over-specifying creates groups you then have to maintain per country.

The default target must be in the group It is an additional annotation on an existing alternate, not a separate page outside the set. If /products/ is the default it must also be listed as a normal alternate with its own language code.

Sitemap annotations are an alternative, not an addition You can declare alternates in the sitemap instead of the HTML, but declaring both invites disagreement. Pick one channel — the reasoning parallels canonical tags vs HTTP Link headers.

Frequently Asked Questions

Is x-default required? Optional, but omitting it means a visitor whose language and country match no alternate gets no guidance and Google chooses arbitrarily. On a site with more than a few locales that arbitrary choice is wrong often enough to justify the annotation.

Should x-default point at a language selector or a real page? A real page, unless the selector genuinely has content. An empty chooser is a poor landing experience and a thin page. Most sites are better served pointing it at their most broadly applicable locale.

Why do geolocation redirects break hreflang? Googlebot crawls predominantly from a small number of locations, so an IP redirect sends it to the same locale regardless of which alternate it requested. The others are never fetched, their annotations never confirmed, and the group is not recognised.

Does x-default affect which page ranks in a matched market? No. Where a locale matches, its own alternate is used. The default only governs the unmatched case, so adding it cannot displace a correctly targeted locale.


Part of: hreflang & International Routing

Related