Redirecting the Root Domain by Accept-Language

This guide, part of Locale Detection & Edge Routing, implements the one redirect that locale detection should make: from the bare domain, example.com/, to the visitor’s locale, example.com/de/. It covers parsing the Accept-Language header correctly, letting a stored preference win, choosing the redirect status, caching the response safely, how crawlers experience it, and when a language selector page is the better choice.

The root URL is special because it carries no locale. Everyone who types the domain, clicks a logo in an old email or follows a link without a path arrives there, and the site has to decide which language to show. A good redirect gets most readers into their language in one hop, remembers when they chose otherwise, and never interferes with URLs that already name a locale.

A first visit and a return visitOn the first visit, the browser sends Accept-Language de-CH, de, en; the edge picks de and responds with a 307 to /de/, which is not cached; the reader switches to French with the switcher, which sets a cookie; on the return visit, the cookie wins and the edge redirects to /fr/.BrowserEdgeSiteGET / (Accept-Language: de-CH, de, en)307 → /de/ (no-store)GET /de/reader picks Français,cookie NEXT_LOCALE=frGET / (cookie fr)307 → /fr/
The header guesses once; the reader's explicit choice wins afterwards.

The Problem

A software company redirected its root with a permanent 301 based on the first language in the Accept-Language header. Browsers cached the permanent redirect, so a reader who once visited with an English browser setting was sent to /en/ forever, even after switching languages on the site. The CDN also cached the redirect under the root URL without varying on the header, so for an hour at a time everyone was sent to whichever language the first visitor after a cache expiry had. The first-value-only parsing sent visitors with de-AT browsers to English, because only de was supported.

How the Root Redirect Should Work

Precedence. Use the preference cookie if it names a supported locale; otherwise parse Accept-Language; otherwise fall back to the default locale. Geolocation, if used at all, comes after Accept-Language and should mostly influence region rather than language.

Correct parsing. Parse all language ranges with quality values, sort by quality, and match exactly first, then by base language, as shown in the locale detection topic.

Temporary redirect. Use 307 or 302. The target depends on the visitor, so a permanent redirect, which browsers and crawlers cache as universal, is wrong.

No shared caching. Mark the redirect Cache-Control: private, no-store, or configure the CDN to include the detection inputs in the cache key for the root only. Never let one visitor’s redirect be served to another.

Everything else untouched. Only the root and other locale-less entry points redirect. Locale-prefixed URLs are served as requested.

Redirect choices and their consequencesPermanent versus temporary redirects, shared versus private caching, and first-value versus weighted header parsing, with the consequence of each choice.ChoiceWrong optionConsequenceSafe optionStatus301 permanentbrowser remembers first guess307 temporaryCachingshared CDN cacheeveryone gets one visitor's localeprivate, no-storeParsingfirst value onlyregional tags miss supported baseweighted, base-language matchScopeevery URLreaders trapped, crawlers confusedlocale-less entry points only
The three defaults that most often go wrong, and the safe alternative for each.

Implementation

For edge platforms without a framework, a small worker handles the root. The parsing function is the same as in the topic page; the worker adds the cookie precedence and safe headers.

TypeScript
// worker.ts: Cloudflare Workers style
import { pickLocale } from "./accept-language";

const SUPPORTED = ["en", "de", "fr", "es", "ja"];
const DEFAULT = "en";

function cookieValue(header: string | null, name: string): string | null {
  const match = header?.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
  return match ? decodeURIComponent(match[1]) : null;
}

export default {
  async fetch(req: Request): Promise<Response> {
    const url = new URL(req.url);
    if (url.pathname !== "/") return fetch(req);                     // only the root is detected

    const saved = cookieValue(req.headers.get("cookie"), "NEXT_LOCALE");
    const locale = saved && SUPPORTED.includes(saved)
      ? saved
      : pickLocale(req.headers.get("accept-language"), SUPPORTED, DEFAULT);

    const target = new URL(`/${locale}/${url.search}`, url.origin);
    return new Response(null, {
      status: 307,
      headers: {
        Location: target.toString(),
        "Cache-Control": "private, no-store",
        Vary: "Accept-Language, Cookie",
      },
    });
  },
};

The switcher sets the cookie whenever the reader chooses a language explicitly, with a long lifetime, SameSite=Lax and the Secure flag. Setting it on the server side of the switch, for example through a small route that sets the cookie and redirects to the chosen page, keeps it working without JavaScript.

How crawlers experience the root

Most search crawlers send no Accept-Language header or a fixed English one and carry no cookies, so they are redirected to the default locale. That is fine: they discover other locales through the hreflang cluster of the default homepage, through sitemaps and through links in the language switcher. Point the homepages’ x-default at either the default locale’s homepage or a language selector page, and make sure the root itself is not listed in sitemaps, since it only redirects.

The language selector alternative

Instead of redirecting, the root can serve a small language selector page listing the available languages, each in its own language, with a remembered preference forwarding returning visitors automatically. The selector costs first-time visitors one click, but it never guesses wrong, it works identically for crawlers and humans, and it makes a natural x-default target. Sites with evenly split audiences, such as in multilingual countries, often prefer it; sites where most visitors share one language usually prefer the redirect.

Other locale-less entry points

The root is the most common locale-less URL, but not the only one. Campaign short links, QR codes on printed material, app deep links and URLs from before the site had locale prefixes all arrive without a locale. Treat genuinely language-neutral ones, such as a QR code on an international product box, like the root: detect and redirect temporarily. Treat language-specific ones, such as a QR code printed on a German flyer, as fixed mappings with a permanent redirect to the German page, because their audience is known when they are created. Encoding the locale directly in new campaign links avoids the question entirely, and is the simplest rule to give marketing teams.

Configuration Reference

Setting Recommendation Why
Scope root and locale-less entry points only URLs with locales are explicit choices.
Precedence cookie, Accept-Language, default Explicit choices win over guesses.
Status 307 or 302 The target depends on the visitor.
Caching private, no-store No cross-visitor leakage.
Cookie long-lived, set only on explicit choice Remembers real preferences.
Sitemap exclude the root It only redirects.

Gotchas & Edge Cases

  • Query strings. Preserve query parameters, such as campaign tags, when redirecting, or analytics loses attribution for root links.
  • Cookie on every visit. Setting the cookie from detection rather than explicit choice freezes the first guess, just like a permanent redirect.
  • Default locale without prefix. If the default locale is served at the root without a prefix, there is nothing to redirect for default-language visitors; redirect only others, and consider a suggestion banner instead.
  • HEAD requests and monitors. Uptime monitors hitting the root see a redirect. Point them at a locale-prefixed page or follow redirects.

Worked Example

The software company changed the root redirect from 301 to 307, marked it no-store, replaced first-value parsing with weighted matching, and set the preference cookie only from the language switcher. Visitors with de-AT and de-CH browsers now reached German, returning visitors who had chosen French reached French, and the CDN stopped serving one visitor’s redirect to everyone. The proportion of root visitors who switched language within their first minute fell from 18 to 4 percent.

Root visitors switching language within a minuteShare of visitors arriving at the root who changed language within their first minute, before and after fixing the redirect status, caching, parsing and cookie handling.Before18 % of root visitorsAfter4 % of root visitors
Most wrong landings came from cached and remembered bad guesses, not from the idea of detection.

Measuring and Tuning

Log each root redirect decision with the signal that decided it, cookie, header or default, and the chosen locale, sampled to keep volume low. Combine it with analytics to see how often readers switch language shortly after landing, per decision source. A high switch rate for header-based decisions for one locale suggests a mapping problem, for example regional tags that match the wrong base language. A high share of default decisions suggests many visitors send headers for unsupported languages, which is useful input for localization planning. Revisit the numbers after adding locales, since a new locale changes the matches for many visitors.

Rollout Checklist

  • Detect only at the root and other locale-less entry points.
  • Prefer the cookie, then weighted Accept-Language, then the default.
  • Respond with a 307 and private, no-store, preserving query strings.
  • Set the preference cookie only on explicit language choices.
  • Exclude the root from sitemaps and point x-default deliberately.
  • Log decisions and watch early language switches.

Frequently Asked Questions

Should the redirect use geolocation?

Rarely for language. Consider it only for choosing between regional variants of the detected language, and never above an explicit cookie.

Is 302 or 307 better?

Both are temporary redirects and safe for this purpose. 307 preserves the request method, which is slightly cleaner; for GET requests they behave the same.

Will the redirect slow down the first visit?

It adds one short round trip at the nearest edge location. A selector page adds a click instead; both are acceptable for an entry point.

Should we redirect again when the header changes?

Only at the root and only without a cookie. Once a reader has chosen, their choice wins until they explicitly choose again in the switcher.

What if a visitor’s language is not supported?

Fall back to the default, or to a selector if you prefer to let them choose among the available languages.