Managing a Multilingual Multisite Implementation With a Dedicated Domain for Each Language

Ok if that title isn't clear enough, let me try again. Our new site has a dedicated domain for each language, so we need to configure the language switcher to point to a FQDN instead of a relative path. There are a few ways to do this, like using configs, but I'm going to show you show to manage this using the site's root node. We're going to also redirect to the right language when context isn't available using IIS.

For the sake of this article let's say we have a multisite implementation, and Site 1 has two domains configured for it. Site1en.com and Site1fr.com will be our domains of choice. For this to work well for the User we need to:

  1. Configure links to include the language in the URL.
  2. Always force the use of language in URLs.
  3. Have the language switcher on the site point to the other domain.

Configuring Links to Include Languages

This one should be pretty easy for you. Setting languageEmbedding to always will do the trick. Here. have a config!

<configuration>  
  <sitecore>
    <linkManager defaultProvider="sitecore">
      <providers>
        <add>
          <patch:attribute name="languageEmbedding">always</patch:attribute>
        </add>
      </providers>
    </linkManager>
  </sitecore>
</configuration>


Forcing the Use of Languages in the URLs

Let's say you have a User go to https://site1fr.com/ without context, so they're going to see the site in English. Or, you have a User switch out /en/ in the URL to /fr/, which would keep them on the English domain in French context. Let's handle this with IIS redirects. 

The following two redirects will handle the case where a request is made to a URL with no context language. It simply observes the absence of en or fr at the beginning of the URL and then redirects accordingly.

  <rule name="ForceLanguageFrench" enabled="true" stopProcessing="true">
    <match url="(.*)" />
    <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
      <add input="{HTTP_HOST}" pattern="^site1fr\.com$" />
      <add input="{REQUEST_URI}" pattern="^/$" />
    </conditions>
    <action type="Redirect" url="fr" />
  </rule>
  <rule name="ForceLanguageEnglish" enabled="true" stopProcessing="true">
    <match url="(.*)" />
    <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
      <add input="{HTTP_HOST}" pattern="^site1en\.com$" />
      <add input="{REQUEST_URI}" pattern="^/$" />
    </conditions>
    <action type="Redirect" url="en" />
  </rule>

Ok we've got the right default context language. Now that we're sure our URLs are always complete, let's switch domains should the wrong language be requested.

  <rule name="RedirectToFrenchDomain" enabled="true" stopProcessing="true">
    <match url="^(fr$|fr/)" />
    <conditions>
      <add input="{HTTP_HOST}" pattern="^site1en\.com$" />
    </conditions>
    <action type="Redirect" url="https://site1fr.com{REQUEST_URI}"></action>
  </rule>
  <rule name="RedirectToEnglishDomain" enabled="true" stopProcessing="true">
    <match url="^(en$|en/)" />
    <conditions>
      <add input="{HTTP_HOST}" pattern="^site1fr\.com$" />
    </conditions>
    <action type="Redirect" url="https://site1en.com{REQUEST_URI}"></action>
  </rule>


Let's Get That Language Switcher Using the Right Domain!

Ok now comes the development work. At the root item I've added a field called “Production URL Prefix”, which will have the domain name in it. Because it's versioned we'll see a different value for each language installed.

This field is used when we get the languages for the site, in the following method. There's more to this method than what will be shared today, but you can see the lan.Url getting set near the end:

public static IEnumerable<Language> GetSupportedLanguages()
        {
            var languages = GetAll();
            var siteContext = new Foundation.Multisite.SiteContext();
            var siteDefinition = siteContext.GetSiteDefinition(Context.Item);
            if (siteDefinition?.Item == null || !siteDefinition.Item.IsDerived(Templates._LanguageSettings.ID))
            {
                return languages;
            }
            var supportedLanguagesField = new MultilistField(siteDefinition.Item.Fields[Templates._LanguageSettings.Fields.SupportedLanguages]);
            if (supportedLanguagesField.Count == 0)
            {
                return Enumerable.Empty<Language>();
            }
            var supportedLanguages = supportedLanguagesField.GetItems();
            languages = languages.Where(language => supportedLanguages.Any(item => item.Name.Equals(language.Name)));
            
            if (Configuration.Settings.GetBoolSetting("Feature.Navigation.PrefixLanguageSwitcherWithProductionUrl", false))
            {
                List<Language> languagesWithPrefixedUrls = new List<Language>();
                foreach (Language lan in languages)
                {
                    using (new Globalization.LanguageSwitcher(lan.TwoLetterCode))
                    {
                        lan.Url = $"{Context.Site.GetRootItem()[Foundation.Multisite.Templates.Site.Fields.ProductionUrl]}{lan.Url}";
                    }
                    languagesWithPrefixedUrls.Add(lan);
                }
                return languagesWithPrefixedUrls;
            }
            
            return languages;
        }

In the above example, Foundation.Multisite.Templates.Site.Fields.ProductionUrl points to the Guid of the field you see in the screenshot. Also, to make this configurable we'll use the following settings value, so developers don't go insane with anger. It's not Role based incase they need to turn it on for testing.

<sitecore>
  <settings>        
    <setting name="Feature.Navigation.PrefixLanguageSwitcherWithProductionUrl" value="false" />    
  </settings>
</sitecore>

Now that the method is available, the rendering will use it like this:

@{
    var activeLanguage = LanguageRepository.GetActive();
    var languages = LanguageRepository.GetSupportedLanguages();
}
@if (languages.Any())
{
    foreach (var language in languages.Where(x => x.Name != activeLanguage.Name))
    {
        <li class="lang"><a href="@language.Url" lang="@language.TwoLetterCode">@language.NativeName</a></li>
    }
}

Back to the output, you can see the proper language URL is generated in the source.

<!-- utility menu -->
<nav role="navigation">
  <ul>
    <li><a href="/en/link1" class="active">Link 1</a></li>
    <li><a href="/en/link2">Link 2</a></li>
    <li><a href="/en/link3">Link 3</a></li></ul></nav>
    <li class="lang"><a href="/fr/link1/" lang="fr">French</a></li>
  </ul>
</nav>

There you have it! Another fun little tweak to a Sitecore implementation. Have fun giving this one a try!