Monday, 7 November 2011

MVC Lower-case URLs

SEO guidelines usually recommend that a site's URLs should be kept in all lower-case. The reason for this is that search engines and web servers alike (IIS is a notable exception) will treat two differently cased URLs as two different resources. While the host name of a URL is case insensitive (i.e. there's no difference between and the resource path is not. Therefore and are considered different resources.

While this might not make much sense semantically, consider this: The world's most widely used web server, Apache, treats URLs as case sensitive. Therefore the above URLs do, in fact, represent two different pages. As such, search engines treat these two URLs as different pages, too - and if your website doesn't care about URL casing you might end up with a split index for your pages.

So - how do we ensure that your website generates only lower-case URLs? With ASP.NET MVC this is easy. All you need is:
  • A LowercaseRoute class
  • An extension method for RouteCollection
  • An extension method for AreaRegistrationContext

And yes - the solution I'm about to detail will work with MVC Areas.

The LowercaseRoute class extends the Route class and basically lets that class do all the work. LowercaseRoute just ensures that the host and path portions of he URL are turned to lower-case while the querystring portion is left alone:

internal class LowercaseRoute : Route
public LowercaseRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler){}

public LowercaseRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler){}

public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
: base(url, defaults, constraints, routeHandler){}

public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
: base(url, defaults, constraints, dataTokens, routeHandler){}

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
var path = base.GetVirtualPath(requestContext, values);

if (path != null)
var virtualPath = path.VirtualPath;

if (virtualPath.LastIndexOf("?") > 0)
var leftPart = virtualPath.Substring(0, virtualPath.LastIndexOf("?")).ToLowerInvariant();
var queryPart = virtualPath.Substring(virtualPath.LastIndexOf("?"));
path.VirtualPath = leftPart + queryPart;
path.VirtualPath = path.VirtualPath.ToLowerInvariant();

return path;

The RouteCollection extension method creates an instance of LowercaseRoute and adds it to the route collection:

public static Route MapRouteLowercase(this RouteCollection routes, string name, string url, object defaults)
if (routes == null)
throw new ArgumentNullException("routes");

if (url == null)
throw new ArgumentNullException("url");

var route = new LowercaseRoute(url, new MvcRouteHandler())
Defaults = new RouteValueDictionary(defaults)

routes.Add(name, route);
return route;

The AreaRegistrationContext extension method calls the MapRouteLowercase extension method on RouteCollection and also adds the current context's AreaName property to the route's DataTokens collection. This second step is crucial for areas to work:

public static Route MapRouteLowercase(this AreaRegistrationContext context, string name, string url, object defaults)
var route = context.Routes.MapRouteLowercase(name, url, defaults, constraints, namespaces);

route.DataTokens["area"] = context.AreaName;
return route;

And that's all you really need. Of course, you may want to add more extension methods of your own so that you can add route constraints or any other data that your route handler may need (such as namespace differentiators). But I'll leave that for you to flesh out on your own. Happy coding!

1 comment:

for IT the said...

I have read your blog its very attractive and impressive. I like it your blog.

Guaranteed SEO services Guaranteed SEO

SEO Company in India SEO Services in India