# General Translation Knowledge Base: Language redirects and SEO: URL, cookie, or Accept-Language?
URL: https://generaltranslation.com/en-GB/kb/locale-routing-seo.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Let the URL decide the language of every page you want indexed, because Googlebot sends no Accept-Language header and crawls mostly from US addresses. Use a saved cookie choice, then Accept-Language, only to pick where a neutral root such as / redirects, with a temporary 302 or 307.
Category: Multilingual SEO
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

All three signals are useful, but they answer different questions. The URL says which version was requested, a cookie says what this visitor chose before, and `Accept-Language` says what their browser is set to. Only the URL looks the same to a crawler as to your visitors, so it has to be the signal that decides what a page contains.

## Why does each language need its own URL?

Google&#39;s guidance on [locale-adaptive pages](https://developers.google.com/search/docs/specialty/international/locale-adaptive-pages) explains the problem. If a page returns different content based on the visitor&#39;s perceived country or language, Google might not crawl, index, or rank every version, because Googlebot&#39;s default IP addresses appear to be in the USA and the crawler sends requests without an `Accept-Language` header. Google recommends separate URLs per locale, annotated with [hreflang](/kb/what-is-hreflang).

Googlebot does also crawl from IP addresses outside the USA, but Google&#39;s [multi-regional sites guide](https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites) says it doesn&#39;t vary the crawler&#39;s location to discover a site&#39;s variations. It recommends different URLs for each language &quot;rather than using cookies or browser settings&quot;.

The consequence is concrete. Suppose `/pricing` returns German to a browser that prefers German and English to everyone else. Googlebot arrives with no `Accept-Language` and no cookie, gets English, and indexes English. The German version exists for users, but Google may not discover or index it.

## What each signal is good for

| Signal               | What it tells you                                        | What a crawler sends                             | Use it for                                          |
| -------------------- | -------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------- |
| URL (`/de/preise`)   | The version that was requested                           | The URL it found in a link, sitemap, or hreflang | Deciding the content: the indexable source of truth |
| Cookie (`locale=de`) | The language this visitor picked earlier                 | None of your visitors&#39; choices               | Remembering an explicit choice on later visits      |
| `Accept-Language`    | The browser&#39;s UI language and configured preferences | Nothing (Googlebot omits the header)             | Suggesting a language on a first visit              |
| IP geolocation       | An approximate country, not a language                   | A mostly US address                              | Suggesting a region; rarely worth it                |

The browser header is weaker than it looks. MDN&#39;s [`Accept-Language` reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language) calls it a hint, notes it&#39;s often out of the user&#39;s control (when travelling, for example), says Safari and Chrome&#39;s incognito mode list only one language, and states that a server should never override an explicit language choice. IP is weaker still: Google calls IP location analysis &quot;difficult and generally not reliable&quot;. (See [why IP geolocation is the wrong signal](/kb/detect-user-language)).

## Precedence: URL, then saved choice, then header

When the signals disagree, resolve them in this order:

1. **The URL.** A request for `/de/preise` gets German, from every browser, every time. This keeps each URL&#39;s content stable, which is what crawlers and caches depend on.
2. **The saved choice.** A cookie or account setting the visitor set by picking a language. It can decide where the neutral root sends them or which alternative to suggest, but it does not rewrite a localised URL they clicked. The [language switcher](/kb/language-switcher) should identify the language currently displayed.
3. **The `Accept-Language` header.** Used only when there&#39;s no saved choice, and only to pick a destination from the root or to offer a suggestion.
4. **The default locale.** The fallback when nothing matches, which is also what Googlebot gets.

A language picked in the switcher navigates to that language&#39;s URL, so rule 1 covers it.

When a lower signal disagrees with the URL, offer the other version instead of switching. A banner reading &#39;This page is available in Deutsch&#39; respects both the link the visitor followed and the language they probably want. The mechanics of reading these signals are covered in [how to detect a user&#39;s browser language](/kb/detect-user-language).

## Should you redirect visitors by browser language?

The common mistake is redirecting localised URLs by header or IP: a visitor with a French browser who opens `/en/pricing` gets bounced to `/fr/tarifs`. Google&#39;s guide tells sites to avoid this, saying redirects based on what you think the user&#39;s language is could keep users and search engines from viewing all versions of the site. A French speaker who wants the English page can&#39;t reach it, and a crawler from a US address can&#39;t reach anything but English.

Two things replace it:

* **Links between versions.** Google recommends linking each page to its other language versions so users can choose. A switcher on every page also gives crawlers a path to every version.
* **One redirect, at a neutral root.** A root URL such as `/` that has no content of its own can redirect by saved choice or header, or show a language selector. You can annotate it as the optional `x-default` in the homepage hreflang cluster. Google describes language selectors and auto-redirecting homepages as use cases; deep pages should keep a relevant fallback for their own content.

### Which status code to use at the root

Use a temporary redirect, `302` or `307`, because the destination depends on who is asking. Google&#39;s [redirects guide](https://developers.google.com/search/docs/crawling-indexing/301-redirects) distinguishes temporary routing from a permanent URL move: temporary redirects favour retaining the source URL, while permanent `301` or `308` redirects are a strong signal to prefer the destination. A permanent redirect from a language-neutral root would therefore send the wrong signal when the destination is only a visitor-specific choice.

## Caching when responses vary

A cache stores responses by URL. If one URL returns different languages depending on a header or cookie, a CDN can hand one visitor&#39;s language to the next. HTTP handles this with the `Vary` response header:

* **`Vary: Accept-Language`** makes caches key the response on the URL plus the header, per [MDN&#39;s caching guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching#vary). Header values vary a lot (`en-US,en;q=0.9` and `en-GB,en;q=0.9,de;q=0.8` are different keys), so the hit rate drops.
* **`Vary: Cookie`** is worse. [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111#section-4.1) only reuses a stored response when the nominated request headers match, and the `Cookie` header carries every cookie, sessions and analytics included. MDN recommends `Cache-Control: private` instead of varying on cookies when responses are personalised.

Locale-in-the-URL lets the translated content be cached by URL. That only makes the whole response safe to share if its other content is also independent of the visitor. In this example, mark the personalised root redirect `Vary: Accept-Language, Cookie` and `Cache-Control: private`. If a server-rendered language suggestion or account content varies by visitor, keep that response private too, or render the suggestion separately on the client.

## A framework-neutral example

This function separates routing from language negotiation. Resolve a saved choice first, then the browser preferences, using the [language-detection guide](/kb/detect-user-language). Pass that supported locale as `preferredLocale`; the function decides whether to redirect, serve, or suggest another version.

```ts title="routeLocale.ts"
const LOCALES = ['en', 'de', 'fr', 'es-MX'];
const DEFAULT_LOCALE = 'en';

type Decision =
  | { action: 'serve'; locale: string; suggest?: string }
  | { action: 'redirect'; status: 302; location: string; headers: Record<string, string> }
  | { action: 'not-found' };

export function routeLocale(pathname: string, preferredLocale?: string): Decision {
  const preferred = LOCALES.includes(preferredLocale ?? '') ? preferredLocale : undefined;

  if (pathname === '/') {
    return {
      action: 'redirect',
      status: 302,
      location: `/${preferred ?? DEFAULT_LOCALE}/`,
      headers: { Vary: 'Accept-Language, Cookie', 'Cache-Control': 'private' },
    };
  }

  const segment = pathname.split('/')[1];
  const urlLocale = LOCALES.find((l) => l.toLowerCase() === segment.toLowerCase());
  if (!urlLocale) return { action: 'not-found' };

  return preferred && preferred !== urlLocale
    ? { action: 'serve', locale: urlLocale, suggest: preferred }
    : { action: 'serve', locale: urlLocale };
}
```

| Path          | Resolved preference       | Decision                                   |
| ------------- | ------------------------- | ------------------------------------------ |
| `/`           | None                      | Temporary redirect to `/en/`               |
| `/`           | `de`                      | Temporary redirect to `/de/`               |
| `/`           | `fr`, from a saved choice | Temporary redirect to `/fr/`               |
| `/de/preise`  | None                      | Serve German                               |
| `/de/preise`  | `fr`                      | Serve German; suggest French               |
| `/en/pricing` | `de`                      | Serve English; suggest German              |
| `/pt/precos`  | None                      | Not found: Portuguese isn&#39;t configured |

The function returns a routing decision; it does not send a redirect or render a page. Apply its status and cache headers in the server integration, and only offer a suggested version if that page is published in the destination language. This root-only pattern is one implementation; other unprefixed paths can also be neutral entry points if the actual content lives at stable localised URLs.

## How General Translation&#39;s gt-next decides the language

In `gt-next`, [`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) checks the locale in the URL before the saved locale cookie and the `Accept-Language` header, so a localised URL serves its own language. The full resolution order and cookie name are in [how to detect a user&#39;s browser language](/kb/detect-user-language).

For public sites, set `prefixDefaultLocale: true` in [`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware). English lives at `/en/about`, French at `/fr/about`, and each content URL keeps its language regardless of the visitor&#39;s preference. Unprefixed paths such as `/about` act as neutral entry points with temporary redirects. The [Next.js SEO guide](/kb/multilingual-nextjs-seo) connects this configuration to canonical URLs, metadata and sitemaps.

The default configuration leaves English unprefixed and may redirect `/about` according to the saved cookie or browser header. Explicitly enabling the prefix avoids using that URL as both English content and a preference-based entry point. To ignore browser-header preferences, set [`ignoreBrowserLocales`](/docs/react/nextjs/config#ignore-browser-locales) to `true` in `withGTConfig`; the URL and saved choice still apply.

## Key takeaways

* Googlebot sends no `Accept-Language` header and mostly crawls from US addresses, so content chosen by header, cookie, or IP may never be indexed in other languages.
* Resolve the language as URL, then saved choice, then `Accept-Language`, then the default, and never let a lower signal override the URL.
* In this pattern, negotiate only at a neutral root using a temporary `302` or `307`; `x-default` is optional. Keep localised content URLs stable and offer alternatives.
* Stable language URLs can share cached content, but visitor-specific redirects, suggestions, and account data still need appropriate cache controls.
* In `gt-next`, the URL locale outranks the cookie and header; `prefixDefaultLocale` and `ignoreBrowserLocales` tighten the rules.

## Related questions

- [How do I detect a user's browser language?](https://generaltranslation.com/en-GB/kb/detect-user-language.mdx): Read the browser's ordered language list, navigator.languages in JavaScript or the Accept-Language header on the server, sorted by q-value, and match it to your supported locales with a fallback. Safari and Chrome incognito report only one language, and IP geolocation reveals location, not language.
- [Language switcher best practices: UX, SEO, and accessibility](https://generaltranslation.com/en-GB/kb/language-switcher.mdx): Name each language in its own language, such as Deutsch or 日本語, and don't use flags, which represent countries. Link each option to the same page in that language with a crawlable link, remember the choice in a cookie, and never auto-redirect a localised URL.
- [What is hreflang, and how do I set it up?](https://generaltranslation.com/en-GB/kb/what-is-hreflang.mdx): Hreflang tells Google which URLs are language or regional versions of the same page, so it can show the right one in search results. Add it with HTML link tags, HTTP Link headers, or a sitemap, make every link reciprocal, and use valid codes such as en-GB, not en-UK.
- [Subdirectory vs. subdomain vs. ccTLD for multilingual SEO](https://generaltranslation.com/en-GB/kb/multilingual-url-structure.mdx): Use subdirectories such as example.com/de/ when one product shares a site and release process. Only a ccTLD signals a country, subdomains allow separate hosting, Google discourages ?lang= parameters, and it treats vanity domains such as .io and .ai as generic.
- [How do I set up SEO for a multilingual Next.js app?](https://generaltranslation.com/en-GB/kb/multilingual-nextjs-seo.mdx): Give each language its own prefixed URL, such as /en/about and /es/about, then publish html lang, a self-referencing canonical, hreflang alternates with x-default, a translated title and description, and a sitemap entry for each published language. Mark unpublished versions noindex. General Translation's gt-next routes each language to its own URL and translates titles with getGT.

## Previous and next questions

- Previous question: [Is AI translation accurate enough for a production app?](https://generaltranslation.com/en-GB/kb/is-ai-translation-accurate.mdx)
- Next question: [Language switcher best practices: UX, SEO, and accessibility](https://generaltranslation.com/en-GB/kb/language-switcher.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/en-GB/kb/sitemap.md) for every article.
