One URL per language, hreflang with return links, self-referencing canonicals, sr-Latn for Serbian and 404 instead of a silent English fallback — the rules behind our English, Serbian and Russian pages.
Our site speaks English, Serbian and Russian. For people that is a language switcher in the header; for search engines it is a set of rules that decides which version shows up for whom. Get them wrong and the Russian page competes with the English one, or a Serbian searcher lands on a page they cannot read. Here is how we set it up.
One language, one address
Every language lives at its own URL: English at the root, Serbian under /sr/, Russian under /ru/. The content of a page never depends on cookies or on the browser language, so a crawler that follows a link always sees the same version a visitor would.
hreflang tells engines which page is for whom
Each page lists all of its language versions — including itself — plus an x-default for everyone else. This is the head of the Russian version of this article:
<link rel="canonical" href="https://onlyapps.org/ru/blog/hreflang-three-languages" />
<link rel="alternate" hreflang="en" href="https://onlyapps.org/blog/hreflang-three-languages" />
<link rel="alternate" hreflang="sr-Latn" href="https://onlyapps.org/sr/blog/hreflang-three-languages" />
<link rel="alternate" hreflang="ru" href="https://onlyapps.org/ru/blog/hreflang-three-languages" />
<link rel="alternate" hreflang="x-default" href="https://onlyapps.org/blog/hreflang-three-languages" />A few details matter:
- Return links. Every version must point to every other version. If the Russian page lists the English one but not the other way round, engines may ignore the pair.
- Self-referencing canonical. Each language version is canonical to itself. Pointing all canonicals to English would tell search engines that the translations are duplicates.
- Language tags. Values follow BCP 47. Serbian is written in two scripts, so we mark our version as
sr-Latn— Serbian in Latin script. - x-default. English is the fallback for languages we do not support.
Untranslated means 404, not a fallback
When a post exists only in English, /ru/blog/... returns 404 instead of quietly showing the English text under a Russian address. That keeps every indexed URL honest: the language in the address is the language on the page. The hreflang set only lists translations that actually exist, and the language switcher sends you to the matching translation — or to the home page in that language when there is none.
Sitemaps repeat the same story
Our sitemaps list every published page in every language and attach the same alternates to each URL. Search engines can then learn the relationships without loading every page, and the dates in lastmod change only when the content changes.
| Check | What correct looks like |
|---|---|
| URL structure | One URL per language, no cookie-based switching |
| hreflang | All versions listed on every version, plus x-default |
| Canonical | Each version canonical to itself |
| Missing translation | 404 in that language, not a copy of English |
| Sitemap | Same alternates as in the page head |
Mistakes we see most often
- Translating the interface but leaving the main text in English under a localized URL.
- Redirecting visitors by IP, which hides the other versions from crawlers.
- Mixing region and language:
ru-RUlimits a page to Russia, whilerutargets Russian speakers everywhere. - Forgetting to update alternates when a translation is removed.
Tip
After any change, open the page source in each language and check that the canonical and the hreflang set are identical across versions — except for the canonical, which always points to the page itself.
Want to see it live? Switch the language at the top of this page — you will land on this article's translation.