Contact

WritingCase study

How we built our bilingual EN/TR site: hreflang, redirects, language detection

By Rues · Published · Updated

This is a record of how we set up the two languages on our own site, ruesandora.com, which went live on 7 October 2026. It covers what we built, why, the mistakes our own checks caught, and what we left out. We have no traffic or ranking data yet, so there are no results here.

Where did we start, and what did we change?

The site launched in Turkish at the root (/, /hizmetler/...) with English added later under /en/. On 7 October 2026 we reversed it: English became the main language at the root, Turkish moved to /tr/. The two languages are equal in weight; English sits at the root because the site is meant to be found in searches outside Turkey too.

We moved on the same day the English pages appeared. A site that is a few hours old has few old addresses in search indexes and few links pointing at them, so a move is cheap. The site had 18 addresses at the time: two home pages and eight service pages in each language.

The address map after the move:

PageEnglishTurkish
Home//tr/
A service/services/seo-geo-aeo/tr/hizmetler/seo-geo-aeo

How do the old addresses redirect?

The site runs on Cloudflare Workers static assets, which read a _redirects file in the site folder. Each line is source destination code. We used 301 for every rule. Google's documentation says a permanent redirect (301 or 308) tells its indexing pipeline that the target should be canonical, while a temporary one (302 or 307) does not. Cloudflare's default code in this file is 302, so the code has to be written on every line.

/en/services/growth /services/growth 301
/hizmetler/buyume /tr/hizmetler/buyume 301
/en/services/* /services/:splat 301
/hizmetler/* /tr/hizmetler/:splat 301
/en/* /:splat 301

Two details from Cloudflare's documentation shaped the file. Static rules have to come before rules with *. And redirects run "regardless of whether or not an asset matches the incoming request", so a careless rule can hide a real page. A test reads the file and fails if any rule's source is a file that still exists, or if /, /tr/, robots.txt or sitemap.xml would be redirected.

Which mistakes did we catch?

Larger changes on this site go through a separate checking agent before they are merged; we describe that setup in another guide. These catches come from the git history of the language move. The first two came from that checker.

Old addresses with a trailing slash took two hops. /hizmetler/seo-geo-aeo/ matched the wildcard rule and went to /tr/hizmetler/seo-geo-aeo/. Cloudflare's default HTML handling then sends /page/ to /page with a 307, which is temporary. The checker asked for one-step redirects. We added a fixed 301 rule for every old address with a trailing slash, plus the bare folders (/hizmetler, /en/services), which now go to the services section of the home page. The test now requires each known old address to have its own fixed rule and to land on an existing page in a single hop.

A notes file would have been public. The site folder holds a README for us. Cloudflare would have served it at /README.md. A .assetsignore file now keeps it out.

Internal links cost a redirect on every click. An earlier research pass found that service pages linked to buyume.html-style addresses. Cloudflare answers those with a 307 to the clean address. We changed the links and added a test that scans every internal link, every sitemap address and every address in llms.txt, and fails if any of them would hit a redirect. The test also checks itself: it confirms that the detector flags a .html link, a _redirects source and a trailing-slash variant.

Testing tools were being redirected. The first version of the language script skipped visitors whose user agent contained words such as bot or crawl. Search Console's URL inspection uses Google-InspectionTool, and Lighthouse uses its own name; neither contains those words. Both could have been redirected to the other language and tested the wrong page. About 20 minutes after the first version, we added Inspection and Lighthouse to the exclusion list.

How is hreflang set up?

Every page carries three link rel="alternate" tags: en, tr and x-default, with x-default pointing to English. The same pairs are written into sitemap.xml. Google says these methods are equivalent and that each version "must list itself as well as all other language versions"; if two pages don't point to each other, the tags are ignored. A test checks that every pair is mutual and that each page's canonical is its own address.

Some pages will exist in one language only. A Turkish article about a Turkey-specific topic may have no English pair. For those pages the generator writes only the page's own hreflang, with no x-default, and the language button goes to the other language's home page. We do not publish half-translated pages. Google treats localized pages as duplicates "only if the main content of the page remains untranslated".

Do we redirect visitors by language?

Partly, and against a general recommendation. Google advises: "Avoid automatically redirecting users from one language version of a site to a different language version." Its reason: "These redirections could prevent users (and search engines) from viewing all the versions of your site." We think our version carries little of that risk: bots are never redirected, Turkish pages never redirect, and both addresses of every page are in the sitemap and in hreflang.

We kept a narrow version. A short script in the page head acts only when all of these are true:

  1. The page is in English. Turkish pages never redirect.
  2. The visitor arrived from another site or typed the address; clicks inside the site never redirect.
  3. Turkish is among the browser's languages. A visitor who pressed TR before is sent to Turkish whatever the browser language is.
  4. The user agent does not look like a bot or a testing tool.
  5. The visitor has not chosen English before. Clicking TR or EN saves that choice in the browser.

Each language keeps its own URL, so search engines see both versions as separate pages. The trade-off is plain: a Turkish speaker who wants English has to press EN once. The redirect also dropped the original referrer, so our cookie-free visit counter would have recorded the wrong source. The script now hands the referrer to the next page, where it is read once and deleted.

Why do sitemap dates come from git?

The same research pass found that all 18 sitemap addresses carried the same lastmod date. Google says it uses lastmod only when the value is consistently and verifiably accurate. The generator now takes each page's date from its last commit in git, in Turkey time. If a page has uncommitted changes, it uses today. A test fails if the sitemap and git disagree, which reminds us to run the generator after editing a page by hand.

What does IndexNow do here?

IndexNow lets a site tell participating search engines that an address changed. A key file sits at the site root, and a small script collects the pages changed since a given commit from git diff and posts them in one request. The protocol allows up to 10,000 addresses per post. Our first submission of all 18 addresses got 202, which the IndexNow documentation defines as "URL received. IndexNow key validation pending." Google is not in the IndexNow list of participating engines, so this does nothing for Google.

What did we leave out?

  • No server-side language switching by IP address or cookie. Each language has its own URL.
  • No machine-translated pages published as they are.
  • No hreflang between pages that are not true pairs.

Open item (7 October 2026): http:// to https:// and www to the bare domain still answered without a redirect; the fix is a Cloudflare panel setting.

What are the results?

We do not know yet. Search Console and Bing data had not arrived when this was written, and one week of data would prove little anyway. When we have dated figures, we will add them to this page with the date of measurement.

If you are planning a similar move, we are happy to compare notes.

Sources

Want to talk about this for your brand?

Write about your project and what you want to do. The message goes straight to Rues.

Open the contact form