Technical SEO for a bilingual website, step by step
A second language doubles your pages and the ways they can go wrong: the Romanian page that points its canonical at the English one, a language switch that drops you on the homepage, alternates that do not link back. This guide walks through the setup of the website you are reading, which is built in English and Romanian, and the automatic checks that keep it correct.
One URL per language, with a clear prefix
Every page exists at `/en/...` and `/ro/...` with a readable slug in its own language: this guide is `/en/insights/bilingual-website-technical-seo/` and `/ro/resurse/seo-tehnic-site-bilingv/`. The root address redirects permanently to `/en/`; nobody is redirected by IP or browser language, and the other language is always one click away.
The pairing is not guessed from slugs. Each page has a stable key in a route registry, and navigation, the language switch, breadcrumbs, canonicals, alternates and the sitemap are all generated from it.
Self-canonicals and reciprocal hreflang
Each language version is its own canonical page; the Romanian page never canonicalises to the English one. Each page lists both versions as alternates, including itself, plus an `x-default` pointing to the matching English page. Because both pages are generated from the same registry entry, the return links cannot drift apart. This follows Google’s documentation on localized versions.
- `<link rel="canonical">`: the page’s own absolute URL.
- `hreflang="en"` and `hreflang="ro"`: both versions, on both pages.
- `hreflang="x-default"`: the English version of that same page, not the homepage.
- `<html lang>` matching the content, and every visible word translated, including image descriptions and form messages.
Sitemaps with honest dates
The sitemap lists only canonical, indexable pages that return 200, with the alternates of each one. Thank-you pages, drafts and redirects stay out. The modification date of a page is set when its content changes, not on every build, so search engines can trust it.
Structured data that matches the page
Every page carries one JSON-LD graph: the Organization, the WebSite, the page itself and its breadcrumb trail, plus Service, Article or VideoObject where the page really is one. Prices in a Service only appear when the same starting price is visible on the page. Nothing describes ratings, reviews, offices or clients that do not exist.
Environment safety: no accidental indexing
Local and preview builds carry `noindex` and use the local origin. A production build refuses to run if the public address is missing, local, private or a placeholder domain, so a canonical can never point to `localhost` or `example.com` by mistake. Indexing is switched on only by an explicit production setting.
Checks that run on every build
After the static build, a script reads every generated page and fails the build if a title or description is missing or duplicated, if there is not exactly one H1, if the canonical is not the page itself, if hreflang return links are missing, if an internal link is broken, if a Romanian page lacks diacritics where expected, or if the sitemap lists a page that is not indexable. Mistakes are caught before anyone deploys.
For AI search (AEO and GEO)
The same foundations serve AI-driven search: content in the initial HTML, clear headings that answer a question, facts with dates and sources, and crawler access you have decided on deliberately. Google’s current guidance says its AI features rely on regular search signals and do not require special markup or an `llms.txt` file. We provide one anyway as a plain map of the site, without expecting it to change rankings.