Hreflang × subdomain × subdirectory: what to pick
Back in April I finished wiring up hreflang on one of my projects — three locales: en, ka, lt. I dropped the tags into the head via Next.js metadata, spot-checked a dozen pages by hand, opened a few URLs in incognito from different IPs. Everything looked right: a Georgian saw ka, a Lithuanian saw lt, the rest of the world saw en. I closed the ticket and moved on.
Two weeks later I open Google Search Console. The International Targeting section is glowing red: "Return tag missing" on 60 URLs. The hreflang tags point from en to ka and lt, but some of those pages don't return the reverse link. Google ignores those relationships on both sides — meaning half my hreflang setup simply doesn't exist as far as Google is concerned. And the whole time, the site kept serving the ka version to US users searching in English.
Multilingual SEO is its own subsystem, with its own files, its own checks, its own URL generation logic. And the URL structure decision — where the locale lives, in the domain, subdomain, or folder — gets made ONCE, at the start. Changing it later means 301 redirects, sitemap migration, re-registering in GSC, and at least six months to recover positions. Backing out is almost impossible.
This post covers the three URL strategies, how they interact with hreflang, and what I picked for hiregora.com.
Hreflang — what it is
Hreflang is a signal to Google: "this page is in this language for this country." That's it. It's not a direct ranking factor, not magic — just metadata that helps Google figure out which of your 3-4 language versions to show a specific user.
The format is standard:
<link rel="alternate" hreflang="ru" href="https://example.com/ru/page" />
<link rel="alternate" hreflang="en" href="https://example.com/en/page" />
<link rel="alternate" hreflang="x-default" href="https://example.com/page" />
You can put it in three places: in the page's <head>, in sitemap.xml as <xhtml:link>, or in an HTTP Link header (for non-HTML files like PDFs). I put it in the head and duplicate it in the sitemap. The duplication is insurance: if JS breaks head rendering on a page, the sitemap still gives Google the right picture.
There are two levels of coverage. Either plain language (en, ru, de) — works for every country that speaks that language. Or language plus region (en-US, en-GB) — pinpoint country targeting. If your content is the same for all English speakers, plain en is enough. If you actually have different pricing for the US and the UK, you need en-US and en-GB as separate pages.
x-default is the fallback. When a user doesn't match any of the declared hreflang combinations, Google shows this version. It usually points at the main locale or a dedicated language picker page.
The key thing: hreflang doesn't affect rankings directly. It affects which version the user sees. They see the right one — they don't bounce in 3 seconds, behavioral signals are good, positions climb indirectly. Broken hreflang = bad UX = behavioral signals tank = positions drop.
The three URL strategies
The URL decision comes before any hreflang setup. You can put the locale in three places: the domain itself, a subdomain, or a folder. All three work with hreflang, but each one interacts differently with domain authority, indexing, and infrastructure.
ccTLD — a separate domain per country
Structure: example.de, example.fr, example.com.br. Each country gets its own domain with a national TLD.
The upsides are serious. ccTLD is the strongest geo signal. .de means Germany, full stop, no GSC configuration needed. Domain reputation accumulates per country: links from German sites to example.de boost the German version and don't spread to the others. And it's legally convenient: you can register a separate domain to a local entity.
The downsides are equally serious. Every ccTLD is a separate domain from scratch. No shared DR. If your main example.com has DR 60, the new example.de starts at DR 0 and you have to build it up from zero with links — six to twelve months minimum. Plus infrastructure: five countries means five DNS zones, five SSL certs, five separate GSC properties.
When to pick this: enterprise with significant per-country content, separate legal subjects, local teams. If you have a German legal entity, local pricing, and a local team, example.de is justified. One product in four languages is overkill.
Subdomain — a subdomain per locale
Structure: de.example.com, fr.example.com, en.example.com. The locale lives in the subdomain.
Upsides: technical isolation. Each subdomain can sit on its own server, its own CMS, its own stack. Handy when de is run by a German team on WordPress while en is run by a different team on Next.js. And it's easy to migrate to ccTLD later: 301 redirect from de.example.com to example.de and part of the authority carries over.
Downsides: Google treats each subdomain as a separate site by default. DR from the main example.com doesn't fully transfer to de.example.com — some of it does, but not the way it would from a folder. If you spent two years building example.com up to DR 50, de.example.com starts somewhere around 20-30. Plus GSC requires you to register each subdomain as a separate property.
When to pick this: when language versions actually have different tech stacks, different teams, different release cycles. Or when you plan to migrate to ccTLD later. Or for a separate product on the same brand — that's what I have with seo.hiregora.com: it's not a language, it's a separate tool with its own codebase.
Subdirectory — a folder per locale
Structure: example.com/de/, example.com/fr/, example.com/en/. The locale is just the first segment of the path.
Upsides: one domain, one domain authority, everything compounds. A link to example.com/de/page boosts the entire domain, including the Russian and English versions. One GSC property, one analytics setup, one SSL cert, one CDN. Administration is an order of magnitude simpler. All content feeds one domain entity.
Downsides: shared infrastructure is both an upside and a downside. The main site goes down, the whole multilingual site goes down. You can't give a local team access only to their folder. The big limitation: geo-targeting in GSC is only configurable domain-wide. You can't say "all of /de/ targets Germany and /en/ targets the US." You can only do it via hreflang with regions — it works, but the signal is weaker than ccTLD.
When to pick this: a single product in several languages, a single team, a single stack. Domain DR is valuable and you don't want to dilute it. Infrastructure simplicity matters more than technical isolation. For 9 out of 10 SaaS or content projects, this is the right call.
What I picked
For hiregora.com it's subdirectory: /en, /ka, /lt, and the root for the Russian version. Here's the reasoning.
One product, one owner, one stack — Next.js 15. One content team (me). No local legal entities, no regional pricing. The textbook setup where subdirectory wins across the board.
The domain DR is valuable — I've been building it up for the last six months, and every article in any language contributes to the same number. If I split languages into subdomains, I'd have to build each one separately — minimum a year to recover positions for the same queries.
Cross-locale links. I often link a Russian post to its English counterpart — the reader sees "available in English" and jumps to the en version. When both live in the same folder, it's just <Link href="/en/writings/..."> with no cross-domain hops. Analytics are unified too, the session doesn't break when the user switches language.
I only use a subdomain for seo.hiregora.com — but that's a separate product, not a language. The SEO checker lives on its own codebase with a different stack. The subdomain is justified there: technical isolation is needed, and DR still partially compounds through the shared parent domain.
If tomorrow I decided to spin up hiregora.de with a local German team, that'd be ccTLD. But that's a 2027 hypothesis. Right now, folders.
Implementation in Next.js 15
In Next.js 15 with the app router, all of this comes together through a dynamic segment app/[lang]/. The folder structure mirrors the URL structure:
app/
[lang]/
page.tsx → /, /en, /ka, /lt
writings/
page.tsx → /writings, /en/writings, ...
[slug]/
page.tsx → /writings/foo, /en/writings/foo, ...
Middleware catches requests, checks the first path segment, and decides whether it contains a valid locale. If not, it determines the language from Accept-Language, sets a cookie with the chosen locale, and redirects. One middleware, one source of truth, works on every page.
generateStaticParams for each page returns the array of every locale — [{lang: 'ru'}, {lang: 'en'}, {lang: 'ka'}, {lang: 'lt'}]. This gives you static generation: at build time, Next.js produces four versions of each page as separate HTML files. No SSR at runtime, no server load — just static files served by the CDN.
The hreflang tags come from a buildAlternates() helper in lib/seo.ts. It takes the current path without the locale and returns an object for the Next.js metadata API:
alternates: {
canonical: 'https://hiregora.com/writings/foo',
languages: {
'ru': 'https://hiregora.com/writings/foo',
'en': 'https://hiregora.com/en/writings/foo',
'ka': 'https://hiregora.com/ka/writings/foo',
'lt': 'https://hiregora.com/lt/writings/foo',
'x-default': 'https://hiregora.com/writings/foo',
},
}
Next.js drops this into the <head> as correct <link rel="alternate"> elements. No manual insertions, no missed pages — because it's part of the metadata, which gets generated automatically for every route.
The sitemap is generated by a separate route handler in app/sitemap.xml/route.ts. For each page it emits not one URL but a block of four locales with <xhtml:link rel="alternate" hreflang="..."> inside. That's insurance: if something breaks in the head, the sitemap still gives Google the right picture of how locales link to each other.
The most common mistake — return tags
This is the red flag I opened the post with. And it's the number one hreflang mistake, the one I've seen on every other project.
The rule is simple and non-negotiable. If page A references page B via hreflang, page B MUST reference page A back. This is called reciprocal linking, and Google enforces it strictly. If B doesn't reference A, Google ignores both references. Not one, both.
Example. The ru page /writings/foo has hreflang tags: ru → ru/foo, en → en/foo, ka → ka/foo, lt → lt/foo. I open en/foo to check — it should have all four too, including the link back to ru/foo. If en/foo only declares en and ka, the en↔ru link is broken, Google drops it, and when someone searches in Russian, my ru page won't get the right signal.
Where do these broken links come from in practice. The most common cause is dynamic generation without a shared source. The en pages use one function to generate hreflang, the ka pages use another (because somebody copy-pasted and forgot to update one of them). They drift apart, and tests miss it because each page is valid in isolation.
The fix is one helper that generates the hreflang block for any locale from the same input — the path without the locale. If the input parameter is the same and the function is the same, every locale returns an identical block of hreflang links. Then reciprocal linking isn't a rule you have to remember, it's a consequence of the architecture.
How to check: through GSC International Targeting, once a week. It shows you how many links Google found, how many of those are broken, and which URLs have the problem. Without that report, you'll only find out about issues when your positions drop.
x-default — when you need it
x-default is a separate hreflang tag for users who don't match any of the declared language combinations. Say you have en, ru, ka, lt. A user from Japan visits with Japanese in their browser. Google checks: en — yes, ru — no, ka — no, lt — no. You don't have Japanese. That's when x-default kicks in: Google shows the version marked as fallback.
Where to point x-default depends on the product. Three options:
-
At the main locale. If 70% of your traffic is Russian,
x-default → /(the Russian version). The Japanese user gets Russian. Not ideal, but at least they land on a working page instead of a 404 or a redirect loop. -
At the English version. If the product is international,
x-default → /en/. The English page works as a universal fallback because pretty much the entire internet population can read some English. -
At a language picker. If all your locales are equally strong and there's no obvious fallback,
x-default → /select-language/. A page where the user picks their language themselves. Less elegant, but more honest than forcing ru on someone who doesn't speak Russian.
I picked option 1: x-default → / (the Russian version). Russian is the primary content language right now, and until the other locales catch up in volume, falling back to Russian makes sense. Once the en version is fully built out, I'll switch x-default over to it.
One more thing: x-default is always a separate tag, not a replacement for the regular hreflang. So a ru page should have both hreflang="ru" (for Russian speakers) and hreflang="x-default" (as fallback). Two tags on the same page is normal.
Bottom line
Picking a URL structure is a one-shot decision you make at the start. ccTLD — for enterprise with local legal entities and a serious infrastructure budget. Subdomain — when stacks or teams genuinely differ, or as a stepping stone to ccTLD. Subdirectory — for everything else, and that's the right call in 9 out of 10 cases.
Hreflang layered on top of any of these works the same way, but requires three things: one shared generator across all locales (so reciprocal linking doesn't break), duplication in the sitemap (insurance against JS issues in the head), and x-default as a fallback. Check things through GSC International Targeting, once a week.
And remember, hreflang isn't about rankings directly. It's about the right person seeing the right page. When it works, behavioral signals are good. When it's broken, Google shows the ru version to a Lithuanian, they bounce in 3 seconds, and your whole SEO foundation crumbles from the top down. For the broader picture of factors, see 30 SEO factors for 2026. For how it interacts with canonical, see when you need a canonical URL. For indexing setup, see what to block in robots.txt.