Using a single domain strategy with multiple languages in EPiServer

by: Ted Nyberg (Hallvarsson & Halvarsson)

Introduction

This post covers one way of enforcing a domain strategy for multi-language (or multi-division) sites.

There are two common scenarios for multi-language site domain strategies:

  1. Use one top domain per language
    In this case, www.mysite.com would probably display the english site version while www.mysite.se would display the swedish site.

  2. Use a single domain with a language-specific URL component
    In this case the english site would have a URL like www.mysite.com/en while the swedish site would have the URL www.mysite.com/sv.

One may debate over which strategy to prefer, but that's a topic for another post. I will instead focus on a technical implementation which makes it easier to enforce the domain strategy that you have chosen for your site, no matter if its built upon EPiServer CMS or not.

Code samples

The code samples in this post are rather lengthy, but the concepts are actually fairly simple.

The concepts

In order to implement a re-usable component for enforcing domain strategies I've implemented two classes: DomainRedirectModule and DomainRedirectModuleSettings.

The DomainRedirectModule is responsible for redirecting the user based on the domain used, for example to use www.mysite.com/sv when the visitor requests the URL www.mysite.se. This is accomplished by looking at the domain used for the request and replacing it if the requested domain matches a rule specified in web.config.

The DomainRedirectModuleSettings class is used to create a new configuration section in the web.config file which will be used to specify the redirect rules (or domain strategy) to use.

Configuration

Once implemented you may specify domain strategy (or redirect) rules within the <Configuration> section of the web.config file like this:

<DomainRedirectModuleSettings Enabled="true">
  <Redirects> 
    <addRedirect
       SourceDomain="www.mysite.se"
       TargetDomain="www.mysite.com/se" />
    <addRedirect
       SourceDomain="www.myothersite.com" 
       TargetDomain="www.mysite.com/othersite" /> 
  </Redirects>
</DomainRedirectModuleSettings>


The DomainRedirectModule class

Ok, bear with me. The DomainRedirectModule class is actually an HttpModule class. This means it will be attached to the project through web.config, and it will handle all requests to the web application to ensure that the correct domain is used.

The source code looks like this:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Configuration;
using System.Web.Configuration;
 
public class DomainRedirectModule : IHttpModule
{
    const string REDIRECTS_APPLICATION_VARIABLE_NAME =
       "domainRedirects"
;

    private DomainRedirectModuleSettings _config;

    #region IHttpModule Members

    void IHttpModule.Dispose()
    {
        _config = null;
    }

    void IHttpModule.Init(HttpApplication context)
    {
        _config = (DomainRedirectModuleSettings)System.Configuration.ConfigurationManager.GetSection("DomainRedirectModuleSettings");

        if (context.Application[REDIRECTS_APPLICATION_VARIABLE_NAME] == null)
        {
            Dictionary<string, string> redirects = new Dictionary<string, string>();

            foreach (DomainRedirect redirect in _config.Redirects)
            {
                if (redirect.SourceDomain!=redirect.TargetDomain)
                    redirects.Add(redirect.SourceDomain, redirect.TargetDomain);
                else
                    throw new ConfigurationErrorsException("DomainRedirectModule configuration error. SourceDomain may not be identical to TargetDomain.");

            }

            context.Application[REDIRECTS_APPLICATION_VARIABLE_NAME] = redirects;
        }

        if(_config.Enabled)
        {
            if (((Dictionary<string,string>)context.Application[REDIRECTS_APPLICATION_VARIABLE_NAME]).Count==0)
                throw new ConfigurationErrorsException("Missing configuration settings for domain redirects.");

            context.BeginRequest += new EventHandler(HandleRequest);
        }
    }

    #endregion

    void HandleRequest(object sender, EventArgs e)
    {
        //Get the request and response objects
        HttpRequest request = ((HttpApplication)sender).Context.Request;
        HttpResponse response = ((HttpApplication)sender).Context.Response;

        //Get all redirect rules from the proper application variable
        Dictionary<string, string> redirects = (Dictionary<string, string>)((HttpApplication)sender).Application[REDIRECTS_APPLICATION_VARIABLE_NAME];

        //Get the requested URL excluding the protocol
        //and ignore any trailing slash
        string requestedUrl = string.Concat(request.Url.Host,request.Url.AbsolutePath);
        if (requestedUrl.EndsWith("/"))
            requestedUrl=requestedUrl.Substring(0, requestedUrl.Length-1);

        //Check if the requested URL matches an existing redirect rule
        if (redirects.ContainsKey(requestedUrl))
        {
            string targetUrl = request.Url.AbsoluteUri.Replace(request.Url.Host, redirects[request.Url.Host]);

            //Redirect to the target domain
            response.StatusCode = 301;
            response.StatusDescription = "Moved Permanently";
            response.RedirectLocation = targetUrl;
            response.Flush();
        }
    }

}

The DomainRedirectModuleSettings class

The DomainRedirectModuleSettings class has one purpose: to create a template for a new configuration section in the web.config file. This enables us to us a structured way of configuring an arbitrary number of domain redirect rules in a consistent manner. The code looks like this (and yeah, it's pretty long - I know):

/// <summary>
/// Used to provide a configuration section in web.config for the DomainRedirectModule class
/// </summary>
public class DomainRedirectModuleSettings : ConfigurationSection
{
    public DomainRedirectModuleSettings()
    {
        
    }

    public DomainRedirectModuleSettings(bool enabled)
    {
        Enabled = enabled;
    }

    [ConfigurationProperty("Enabled",
    DefaultValue = true, IsRequired = true)]
    public bool Enabled
    {
        get
        { return (bool)this["Enabled"]; }
        set
        { this["Enabled"] = value; }
    }

    [ConfigurationProperty("Redirects",
    IsDefaultCollection = false)]
    [ConfigurationCollection(typeof(DomainRedirectCollection),
        AddItemName = "addRedirect",
        ClearItemsName = "clearRedirects",
        RemoveItemName = "RemoveRedirect")]
    public DomainRedirectCollection Redirects
    {
        get
        {
            DomainRedirectCollection redirects =(DomainRedirectCollection)base["Redirects"];
            return redirects;
        }
    }
}

/// <summary>
/// Represents a list of redirect rules based on the host name used
/// </summary>
public class DomainRedirectCollection : ConfigurationElementCollection
{
    public DomainRedirectCollection()
    {

    }

    protected override ConfigurationElement CreateNewElement()
    {
        return new DomainRedirect();
    }

    public override ConfigurationElementCollectionType CollectionType
    {
        get
        {
            return ConfigurationElementCollectionType.AddRemoveClearMap;
        }
    }       
    
    public DomainRedirect this[int index]
    {
        get
        {
            return (DomainRedirect)BaseGet(index);
        }
        set
        {
            if (BaseGet(index) != null)
            {
                BaseRemoveAt(index);

                BaseAdd(index, value);
            }
        }
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return ((DomainRedirect)element).SourceDomain;
    }

    new public DomainRedirect this[string SourceDomain]
    {
        get
        {
            return (DomainRedirect)BaseGet(SourceDomain);
        }
    }

    public int IndexOf(DomainRedirect redirect)
    {
        return BaseIndexOf(redirect);
    }

    public void Add(DomainRedirect redirect)
    {
        BaseAdd(redirect);
    }

    protected override void BaseAdd(ConfigurationElement element)
    {
        BaseAdd(element, true);
    }

    public void Remove(DomainRedirect redirect)
    {
        if (BaseIndexOf(redirect) >= 0)
            BaseRemove(redirect.SourceDomain);
    }

    public void RemoveAt(int index)
    {
        BaseRemoveAt(index);
    }

    public void Remove(string sourceDomain)
    {
        BaseRemove(sourceDomain);
    }

    public void Clear()
    {
        BaseClear();
    }
}

/// <summary>
/// Represents a single redirect rule based on the hos name used
/// </summary>
public class DomainRedirect : ConfigurationElement
{
    public DomainRedirect()
    {

    }

    public DomainRedirect(string sourceDomain, string targetDomain)
    {
        SourceDomain = sourceDomain;
        TargetDomain = targetDomain;
    }

    [ConfigurationProperty("SourceDomain", DefaultValue = "www.example.se", IsRequired = true)]
    [StringValidator(InvalidCharacters = "~!@#$%^&*()[]{};'\"|\\", MinLength = 1, MaxLength = 60)]
    public String SourceDomain
    {
        get
        { return (String)this["SourceDomain"]; }
        set
        { this["SourceDomain"] = value; }
    }

    [ConfigurationProperty("TargetDomain", DefaultValue = "www.example.com/se", IsRequired = true)]
    [StringValidator(InvalidCharacters = "~!@#$%^&*()[]{};'\"|\\", MinLength = 1, MaxLength = 60)]
    public String TargetDomain
    {
        get
        { return (String)this["TargetDomain"]; }
        set
        { this["TargetDomain"] = value; }
    }
}

Applying the final touches

Enable our custom configuration section in web.config

In order to enable us to use our newly created configuration section we have to add the section type to the <ConfigSections> section of web.config (located at the top):

<configuration>
   <configSections>
      <section name="DomainRedirectModuleSettings" 
type="MyNamespace.DomainRedirectModuleSettings, MyNamespace" 
requirePermission="false" />
   </configSections>
</configuration>


Now we've specified that we want a new configuration section named DomainRedirectModuleSettings which (as its name implies) is based on the type (or class) DomainRedirectModuleSettings (the names do not necessarily have to match).

We may now declare this configuration section like so:

<DomainRedirectModuleSettings Enabled="true">
  <Redirects> 
    <addRedirect
       SourceDomain="www.mysite.se"
       TargetDomain="www.mysite.com/se" />
    <addRedirect
       SourceDomain="www.myothersite.com" 
       TargetDomain="www.mysite.com/othersite" /> 
  </Redirects>
</DomainRedirectModuleSettings>

Add the HttpModule to your web application

In order to enable a web application to make use of our DomainRedirectModule HTTP module we have to add a reference to it in web.config, within the <httpModules> section:

<httpModules> 
  <add name="DomainRedirectModule" type="MyNamespace.DomainRedirectModule" />
  <!-- Other HTTP modules go here -->
</httpModules>


Go ahead and try it!

I realize that the code samples in this post are relatively massive, at least in comparison to most other blog post code samples. But don't be discouraged by it - they're not even remotely as complex as their sheer row size indicates! ;)

Go ahead and comment on the post if you run into any trouble!

04 April 2008


Comments

  1. Nice!

    If your read the help at Google Webmaster tools you can see that they clearly recommend one locale per hostname before having the a path fragment indicating locale.

  2. Good point, Fredrik! Using this implementation for language separation is only one application, though. I actually wrote it for a company that had multiple domains, such as www.somebusiness.com, but we wanted all sites under the corporate domain, so we would get something like www.corporation.com/somebusiness.
  3. Actually got it working after a couple of tries with no errors (not finding the class, etc etc.) tho the redirect doesn't occur. Any idea what that might be without me giving you the specifications? All the code is almost identical to yours and the namespace is default (EPiServer.Templates.Public) and I made a DomainRedirectModule.cs.
  4. My plan is to make a small demo Visual Studio project available for download. Until then - have you added the HTTP module to web.config? In that case, I would start by adding a breakpoint to the HandleRequest method in the DomainRedirectModule class to see if it executes.
Post a comment    
User verification Image for user verification  
Ted Nyberg (Hallvarsson & Halvarsson)

About me

I'm a consultant at Hallvarsson & Halvarsson where I try to find (or come up with) ways to improve corporate communications online.

I cross-post and publish additional articles on tednyberg.com.

You'll find my contact details here.

Hallvarsson & Halvarsson logo

EMVP, EPiServer Most Valued Professional

MCPD, MCTS and MCP logos

Follow me on Twitter

Bloggtoppen.se

Add to Technorati Favorites

Syndications


Archive


Tag cloud

EPiTrace logger