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:
- 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.
- 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!