Multilingual website SEO: hreflang, translated URLs and Basque, explained with this site as the example
Hreflang, translated URLs, x-default, Basque: a website in several languages that Google understands, with a real site as the example and a checker tool.
Written by Eztebe
A multilingual website that ranks in every language comes down to five rules: one URL per language, translated; hreflang tags that form a complete circle; no redirect based on the browser's language; a language switcher that leads to the same page; and copy proofread by someone who speaks the language.
This site is the example: three languages, and everything below is taken from its code. At the bottom of the article, a tool checks your own tags.
Key points
- Google does not index "translations" but pages: each language has its own URL, title, description and content, one language per page.
- Hreflang tags form a circle: every version references all the others and itself, plus an
x-defaultversion. One missing return link, and the pair is ignored. - No automatic redirect based on browser language or IP address: Googlebot crawls from the United States and would never see your French pages.
- The language switcher leads to the same page in the other language, never the home page; and a flag is not a language.
- For Basque, as for any language you don't speak: translate fewer pages, but have them proofread. A bad version is worth less than no version.
A multilingual website is not a translated website
To Google, a language is a set of distinct pages, each with its own address, not a button that swaps the text on the same page. A site in three languages is therefore three sites that answer each other, joined by tags search engines understand. Everything else in this article follows from that idea.
Three common practices produce no indexable page at all: the little "translate this page" widget that swaps the text with JavaScript, side-by-side translations in the same column, and raw machine translation pasted in without review. In all three cases Google sees one page, in one language, sometimes two mixed together, and your French clients don't find you.
A real multilingual website costs more than translating the copy, which is why I price it separately, 20 to 25% per language. That surcharge pays for what follows.
Where does the language go in the URL?
In a directory, almost always: /en/, /fr/, /eu/ on a single domain. It's the simplest to manage, and all the authority the site earns (links, age) benefits every language. One domain per country is justified for genuinely distinct brands or offers, not for a practice that talks to its clients on both sides of the Bidasoa.
The four ways to place the language, and when to choose each.
| Structure | Example | When |
| --- | --- | --- |
| Directory | eztebe.com/en/ | The default choice: one domain, one authority, one admin |
| Country domain | example.fr, example.es | Different offers, teams or brands per country |
| Subdomain | en.example.com | Rarely useful today; inherits the main domain's authority less well |
| Parameter | example.com/?lang=en | Avoid: Google advises against it, and the URLs are unreadable |
A word on .eus. That domain exists for the Basque-speaking community, and it makes sense for a site written in Basque. I chose eztebe.com, not eztebe.eus: this site isn't in Basque, and a .eus without Basque would be a promise not kept.
Translated URLs, not just prefixed ones
A translated URL contains the word people type in their language. My "business website" page is called /en/services/business-website, /fr/services/site-vitrine and /es/servicios/web-corporativa: three different names, because the keyword is not the same in the three markets. A mere prefix (/en/services/site-vitrine) would give an English page with a French URL.
Technically, this takes a lookup table between the internal path and each language's public path. On this site, it fits in one file:
// src/i18n/routing.ts (excerpt)
export const routing = defineRouting({
locales: ["fr", "es", "en"],
defaultLocale: "fr",
localePrefix: "always",
localeDetection: false,
pathnames: {
"/services/[slug]": { fr: "/services/[slug]", es: "/servicios/[slug]", en: "/services/[slug]" },
"/projets": { fr: "/projets", es: "/proyectos", en: "/projects" },
"/a-propos": { fr: "/a-propos", es: "/sobre-mi", en: "/about" },
"/contact": "/contact",
},
});
The [slug] itself is translated: site-vitrine becomes business-website. That table is then used three times: for internal links, for hreflang tags and for the language switcher. That is what guarantees they all tell the same story.
The hreflang circle
The hreflang tag tells Google: "this page also exists in French, at this address". Every version of the page carries the full list of versions, itself included, plus a default version, x-default. Google only accepts the relationship if it is reciprocal: if the French page doesn't reference the English one, the pair is ignored.
There are three ways to declare these tags: in the page's <head>, in an HTTP Link header, or in the sitemap. One is enough; two do no harm if they say exactly the same thing, but that's double maintenance. This site uses the <head> and the sitemap, generated by the same code, so they cannot disagree.
Codes follow ISO 639-1 for the language, with an optional ISO 3166-1 region: en, fr, eu, en-GB. A region on its own (GB) is not valid; en_GB with an underscore isn't either. Add the region only when the content really differs from one country to another: en-US for a page with American prices and spelling; otherwise plain en, which covers every English speaker.
FR version
eztebe.com/fr/services/site-vitrine
- hreflang="fr"/fr/services/site-vitrineitself
- hreflang="es"/es/servicios/web-corporativa
- hreflang="en"/en/services/business-website
- hreflang="x-default"/fr/services/site-vitrine
ES version
eztebe.com/es/servicios/web-corporativa
- hreflang="fr"/fr/services/site-vitrine
- hreflang="es"/es/servicios/web-corporativaitself
- hreflang="en"/en/services/business-website
- hreflang="x-default"/fr/services/site-vitrine
EN version
eztebe.com/en/services/business-website
- hreflang="fr"/fr/services/site-vitrine
- hreflang="es"/es/servicios/web-corporativa
- hreflang="en"/en/services/business-websiteitself
- hreflang="x-default"/fr/services/site-vitrine
The three lists are identical apart from one line: every page appears in them, including itself, and x-default names the French version, the one served when no language matches. A real example, taken from this site.
On this site, the list is built by a single function, called by every page. It produces the canonical, the hreflang tags and the x-default, and leaves out the languages in which the page doesn't exist:
// src/lib/seo.ts (excerpt)
const languages: Record<string, string> = {};
for (const l of locales) {
const h = hrefFor(l); // null if the page doesn't exist in that language
if (h) languages[l] = absoluteUrl(l, h);
}
languages["x-default"] = absoluteUrl(defaultLocale, hrefFor(defaultLocale)!);
return {
alternates: { canonical: absoluteUrl(locale, hrefFor(locale)!), languages },
};
What comes out in the HTML of the English page:
<link rel="canonical" href="https://www.eztebe.com/en/services/business-website">
<link rel="alternate" hreflang="fr" href="https://www.eztebe.com/fr/services/site-vitrine">
<link rel="alternate" hreflang="es" href="https://www.eztebe.com/es/servicios/web-corporativa">
<link rel="alternate" hreflang="en" href="https://www.eztebe.com/en/services/business-website">
<link rel="alternate" hreflang="x-default" href="https://www.eztebe.com/fr/services/site-vitrine">
Two details make the difference. The URLs are absolute, with the www and no trailing slash, exactly as the site serves them: a URL that redirects breaks the circle. And each version's canonical points to itself, never to the French version: a canonical that names another language tells Google to ignore everything else.
The sitemap repeats the same list for every URL, with the content's real last-modified date:
// src/app/sitemap.ts (excerpt): one entry per language, each with all the alternates
locales.map((l) => ({
url: absoluteUrl(l, hrefFor(l)!),
lastModified: contentUpdatedDate,
alternates: { languages }, // fr, es, en, x-default
}));
Don't redirect based on the browser's language
It's tempting to send a French visitor to /fr/ as soon as they arrive. Google advises against it, for a simple reason: Googlebot crawls from the United States, in English. With an automatic redirect it would never see /fr/ or /es/, and your French pages wouldn't exist for it. The same goes for a redirect based on IP address.
So this site turns detection off (localeDetection: false), and its root redirects to /fr permanently, with a 308, because the default language is fixed. Every URL always answers the same thing, to everyone. If you want to guide the visitor, a discreet banner "cette page existe en français" does the job without breaking anything.
The language switcher leads to the same page
The language switcher is a link to the same page in the other language: the "business website" page in French, not the French home page. A visitor who changes language and lands on the home page leaves. Each link carries the hreflang and lang attributes of its target language, and its label is written in that language: "Français", not "French".
No flags. A flag denotes a country, not a language: a Basque reader lives in Spain or in France, and a Spanish speaker may be in Buenos Aires. The word is enough, and everyone can read it.
A trap I hit on this site: at static generation time, the internal path (/services/web-corporativa) and the public path didn't match on the Spanish pages, and the HTML served to crawlers contained switcher links returning 404 (/fr/services/web-corporativa). Nothing visible in the browser, where everything was recomputed on the client.
A crawl with Screaming Frog revealed it. Since then, the switcher relies on the slug lookup table, identical at build time and in the browser. The lesson: check the raw HTML, not just the screen.
Basque, and any language you don't speak
In the Basque Country, a Basque version is a mark of respect, and often expected by institutions. But a sloppy Basque version does more harm than no version at all: it tells the reader they weren't taken seriously.
I don't speak Basque. When a client needs it, I have it translated by a translator, and I integrate their work with the same tags as the other languages.
Three rules for a language you don't master:
- Translate less, but well. The home page, the services, the contact page, the legal notice. Blog articles only when they make sense for that readership. A page that doesn't exist in a language simply doesn't appear in its hreflang circle, and that's fine.
- One language per page. No Spanish paragraph in the middle of a Basque page. An isolated term in another language is marked with
lang:<span lang="eu">amatxi</span>. - No raw machine translation online. Google treats machine translations published at scale without review as low-value content. If a version hasn't been proofread, set it to
noindexor don't publish it yet.
The code for Basque is eu, with no region: Basque is the same on both sides of the border. If the legal notice or the prices differ between the version for Spain and the one for France, those are two pages, eu-ES and eu-FR. Otherwise, just one.
Metadata, language by language
Each version has its own title, description and share image, written from the keywords searched in that language, not translated. "Business website", "site vitrine" and "web corporativa" are not translations of each other: they are the words each market types into Google. The page's lang attribute follows BCP 47 (en-GB, fr-FR, es-ES), and the inLanguage code in the structured data says the same thing.
The details that give a translation away: dates ("30 October 2026" versus "October 30, 2026"), numbers (€3,000 versus 3 000 €), punctuation (the spaces before ? and : in French, the opening ¿ in Spanish), quotation marks. The browser can do all of this with the Intl API, as long as you ask it to.
The mistakes I see most often
- Hreflang without a return link. The English page references the French one; the French one doesn't reference the English one. The pair is ignored. Remedy: a single function generates the list for all versions.
- A canonical pointing to the main language. Every version points to
/fr/. Google only indexes French. Each version is canonical to itself. - No
x-default, or anx-defaultthat redirects. Name a real page that answers with a 200. - Made-up codes.
GB(region alone),en_GB(underscore),ukfor the United Kingdom (it'sen-GB;ukis Ukrainian). - Relative URLs, or URLs that redirect. Hreflang tags must point to the final, absolute URL, as served.
- A redirect based on language or IP. See above: Googlebot will only see one language.
- Two languages on one page. Menu in Spanish, text in French, because the translation is "in progress". To Google, that page is in no language at all.
- A switcher to the home page. The visitor loses their page, and you lose the visitor.
- A sitemap without alternates. Not wrong, but it misses a chance to confirm the circle.
Check your site
Search Console has had no dedicated hreflang report since 2022. What's left are crawlers (Screaming Frog, Ahrefs) and this little tool, which does what Google does: it reads a page, opens each of its versions and checks that the circle closes.
Paste a page address: the tool reads its hreflang tags, opens each version and checks that the circle is complete. Nothing is stored.
Read the errors first: missing return link, canonical pointing elsewhere, invalid code, version that redirects. Warnings (missing x-default, missing lang attribute) don't block indexing, but they take five minutes to fix. Test a service page rather than the home page: that's where the errors hide, because the slugs are translated there.
Frequently asked questions
Do I need one domain per language?
Rarely. Directories (/en, /fr, /eu) on a single domain are simpler to manage and concentrate the authority the site builds up. One domain per country is justified when offers, teams or brands genuinely differ from one country to another.
hreflang en or en-GB?
en, unless the content differs by country. en covers every English speaker, from Dublin to Sydney. en-US makes sense for a page with American prices, spelling or terms, and only if an en or en-GB page exists alongside it.
Does Google penalise machine translation?
Not as such. What Google filters is content published at scale without added value, and unreviewed machine translation falls into that category. Translate with whatever tools you like, have the result proofread by someone who speaks the language, and don't index what hasn't been reviewed.
How many pages should be translated?
The ones that bring clients: the home page, the services, the contact page, the legal notice. Blog articles only when they make sense for that market; a page missing in a language simply drops out of its hreflang circle. Five accurate pages beat forty approximate ones.
How much does an additional language cost?
On a site I build, 20 to 25% of the site's price per language, translation included when I do it myself (English, French, Spanish). The details are in my price list.
Do I need a .eus?
If the site is written in Basque, or if Basque identity is at the heart of the brand, yes. Otherwise a .com or a .es with an /eu/ directory does exactly the same job for Google, without a promise the site doesn't keep.
What next
If your site already exists in several languages, run it through the tool above: in a minute you'll know whether the circle is complete. If it still has to be built, that's what my multilingual website page is about, and you can tell me about your project: we talk for an hour, and you receive a quote within 48 hours.
- Multilingual
- hreflang
- SEO
- Next.js
- Basque