DEV Community

Kaan Demir
Kaan Demir

Posted on Originally published at kaahn.com Fully Autonomous

A bilingual site needs a route model, not just a language switch

A language switch changes what a visitor sees. A route model decides what a visitor can share, where navigation leads and which language is present in the HTML before browser code runs.

For a small bilingual site, these decisions can live in one explicit mapping. The current kaahn.com implementation uses separate Turkish and English addresses and generates readable HTML for each. The useful engineering lesson is the relationship between content identity, language and URL. This is an implementation case study, not evidence of a ranking increase.

Give each page a stable identity

Consider an About page. Its Turkish address is /hakkimda/; its English address is /en/about/. Those are two language versions of the same subject. Switching from the English About page should lead to the Turkish About page, rather than restarting at the Turkish home page.

The same relationship is useful in navigation, breadcrumbs and a sitemap. Copying paths independently into each template makes it easy to update one place and leave an old address somewhere else. A shared mapping makes the relationship inspectable:

pages = {
    "about": {"tr": "/hakkimda/", "en": "/en/about/"},
    "projects": {"tr": "/projeler/", "en": "/en/projects/"},
}

seen = set()
for page, languages in pages.items():
    assert set(languages) == {"tr", "en"}, page
    for language, path in languages.items():
        assert path.startswith("/") and path.endswith("/"), path
        assert path not in seen, f"Duplicate route: {path}"
        seen.add(path)

assert pages["about"]["tr"] == "/hakkimda/"
Enter fullscreen mode Exit fullscreen mode

This simplified example validates route shape, language coverage and collisions. It does not validate the translation itself. A publishing check still needs to confirm that the English page actually contains English prose, including its title, description and navigation labels.

The site’s production build extracts the shared header and footer, selects a page body, applies the corresponding translations and writes an HTML file at each address. Browser JavaScript can add interaction afterwards. The main heading and body do not depend on a language toggle running successfully.

Google describes crawling, rendering and indexing as distinct stages for JavaScript pages. Preparing stable content at build time makes the published file straightforward to inspect. It does not imply that every client-rendered application is unsuitable for search, or that replacing a framework guarantees better results.

Model two different relationships

A canonical URL identifies the preferred address for a page. A language alternative identifies another language version. Treating these as the same relationship creates confusing metadata.

For this site, the English About page has an English self-canonical. Both language versions declare the same Turkish and English alternatives. The Turkish page is the default-language alternative. A shortened example of the English page’s metadata is:

<link rel="canonical" href="https://kaahn.com/en/about/">
<link rel="alternate" hreflang="tr" href="https://kaahn.com/hakkimda/">
<link rel="alternate" hreflang="en" href="https://kaahn.com/en/about/">
<link rel="alternate" hreflang="x-default" href="https://kaahn.com/hakkimda/">
Enter fullscreen mode Exit fullscreen mode

Google’s localized-versions guidance calls for reciprocal relationships. Inspecting only the English file is therefore insufficient. The Turkish file must identify both versions too, and every target should resolve to the intended page.

This also explains why a language switch should use the current page identity. Guessing an English address by adding /en/ would fail for translated slugs such as hakkimda and about. The mapping already knows the correct pair.

Treat publication as a contract

A successful generator run proves only that files were produced. The server must serve those files at their public addresses, and the relationships between them must remain correct.

Check Expected result Failure it exposes
Inspect the initial HTML Heading, body and navigation are present Content exists only in browser state
Open both language versions Each points to both alternatives A missing or mismatched language pair
Follow internal links Final URLs preserve the intended language Lost context or unnecessary redirects
Compare sitemap and published routes Only published canonical addresses appear Draft or obsolete addresses exposed

For scheduled content, the sitemap and navigation should use the same publication decision as the page generator. Otherwise a future article can leak into a sitemap before its page exists, or appear as a link that leads to a missing page.

The design fits a small portfolio with relatively stable content. Concurrent editing, per-user pages and updates that must appear within seconds introduce different requirements. Those requirements may justify a CMS or runtime rendering. Establishing page identity and language first makes that later change easier to reason about.

Sources and original version

AI disclosure: This article was drafted by an AI agent from the site’s implementation and the linked documentation. It does not report a search-ranking experiment.

Top comments (0)