illustration: Adrien Griveau

Making your website multi-regional using top-level domains

Nicolas Mondollot
Unexpected Token

--

Hi there. My name is Nicolas Mondollot, I am the CTO of Drivy — an awesome peer-to-peer car rental service. We launched our german website in early 2015. Here are some things we learned along the way.

When you start a website you usually start small: one website, hosted on one domain, targeting one country and one language. Simple, right?

Then, one day you decide to go international (exciting!). One problem, though: people don’t all speak/read the same language everywhere. You must localize you content in multiple languages.

The standardized way to do so is to use a locale for each language and geographical region you are targeting. For instance if you want to make your website available in the US, France and Canada you’ll need 4 locales:

  • en : English (US)
  • fr : French (France)
  • fr-CA : French (Canada)
  • en-CA : English (Canada)

side note: this is a ‘pragmatic’ approach to locale naming, where we drop the regional part when it is not needed (more information here).

Great. Now, how do you make your website available in all these locales?

Locales and URLs

The first rule of Search Engine Optimization when you go international is: the locale must appear in the URL. Google suggests choosing among several options:

  • URL parameters: example.com?locale=fr
  • Subdirectories with a generic top-level domain (gTLD): example.com/fr/
  • Subdomains with a generic top-level domain (gTLD): fr.example.com
  • Country-specific top-level domains (ccTLD): example.fr

There is no one-size-fits-all approach. The best solution really depends on your specific requirements (this article from Moz may give you some pointers). At Drivy we decided to use ccTLDs because it was the best solution SEO-wise to target different countries. Plus it’s prettier.

First implementation

In Rails this is pretty straightforward. You just need to point your new domain DNS to your application server and drop this code in your ApplicationController (inspired by the official Rails guide):

class ApplicationController < ActionController::Base
before_action :set_locale

def set_locale
I18n.locale = request.host.split('.').last || :en
end
end

We basically take the last part of the URL domain (fr in the case of example.fr) and we set the locale to this value. Simple.

It works, but it has major drawbacks:

  • it doesn’t work for ccTLDs that don’t correspond to a valid locale (e.g. example.ca)
  • it doesn’t work with second-level domains (e.g. example.co.uk)
  • it doesn’t work in dev environment (where you usually use http://localhost:3000)

One other thing that may cause trouble down the road: what if you can’t map a ccTLD to a given locale? This can happen in several scenarios:

  • the ccTLD is already taken by somebody else — in which case the next best solution is to use a subdomain
  • you want to offer several locales for a given ccTLD (e.g. French and English for Canada)

And this gets even more complex as you add different environments (production, staging, dev) and more subdomains.

There must be a more robust way.

Second implementation

Let’s be more explicit and define a clear one-to-one mapping between the locale and the host:

HOSTS_MAPPING = {
'en' => 'example.com',
'fr' => 'example.fr',
'fr-CA' => 'fr.example.ca',
'en-CA' => 'en.example.ca'
}

Now let’s use this new mapping in our ApplicationController:

class ApplicationController < ActionController::Base
before_action :set_locale
def set_locale
I18n.locale = HOSTS_MAPPING.invert[request.host] || I18n.default_locale
end
end

We can now easily make this work in our staging environment by using a different hash:

HOSTS_MAPPING_STAGING = {
'en' => 'staging.example.com',
'fr' => 'staging.example.fr',
'fr-CA' => 'fr.staging.example.ca',
'en-CA' => 'en.staging.example.ca'
}

side note: this breaks dev/prod parity, so you should probably avoid this and instead buy a dedicated domain for your staging environment

Same thing for our dev environment:

HOSTS_MAPPING_DEV = {
'en' => 'dev-example.com',
'fr' => 'dev-example.fr',
'fr-CA' => 'fr.dev-example.ca',
'en-CA' => 'en.dev-example.ca'
}

side note: this means every member of your dev team must setup his etc/hosts to make all the domains above point to his/her local machine

One major gotcha: emails

Our implementation works pretty well and is pretty flexible. But there is one big gotcha: what host do we use when we send emails to our users?

The first solution that comes to mind is to use the default host and then redirect the user based on his latest locale:

One big problem: although it is easy to share cookies across subdomains, it’s impossible to share cookies across top-level domains. And since server-side sessions are usually based on cookies this means you cannot have proper sessions across all your domains — unless you implement some kind of OAuth mechanism, which isn’t trivial.

If one our your users signs in on your French website (example.fr) he won’t be signed in on example.com, which means you won’t be able to retrieve his locale and you won’t be able to automatically redirect him to the correct domain. Bummer.

One solution is to use the correct domain in your emails. But since we are sending emails asynchronously we can’t rely on the request object like we did previously inside our ApplicationController.

We can work around this by hacking ActionMailer a bit:

class MyMailer < ActionMailer::Base
def welcome(user)
@user = user
mail(to: user.email)
end
def url_options
super.merge({host: HOSTS_MAPPING[@user.locale]})
end
end

We retrieve the user’s locale (stored in our database) and we set the default host dynamically. All the links present in the email will use the correct host. It works!

More gotchas?

A few other gotchas to be aware of:

  • You must set up multiple analytics accounts, one for each domain.
  • You must buy a multi-domain SSL certificate (unless you are ok with managing one certificate per domain). If you want to secure multiple domains AND multiple subdomains things can get very expensive very quickly. At Drivy we chose to use a GeoTrust True BusinessID certificate because it is possible to add up to 25 domains or subdomains for a reasonable price: around €300 for a 5-domain pack, plus €20 per additional domain. This is still very expensive, but there aren’t a lot of alternatives apparently.
  • Your SEO juice will be split between your domains — there is no cumulative effect. If you open a new country you will start from zero SEO-wise. One way to mitigate this is to use alternate hreflang.

Despite all the efforts and all the gotchas I am pretty happy about the way it turned out for Drivy. Top-level domains really are a great strategy for the long term as it gives you clean URLs and a strong branding in each country.

If you are unsure about the strategy you want to adopt I really recommend reading this article from Moz as it will give you concrete pointers.

Happy internationalization!

This article is part of the publication Unexpected Token powered by eFounders — startup studio. All eFounders’ startups are built by extraordinary CTOs. If you feel like it could be you, apply here.

You like? Hit “Recommend” and subscribe to our collection!

--

--