So Su Team : Web Development Tags : Web Development

Using MVC routing for SEO Friendly URLs on Multilingual sites

So Su Team : Web Development Tags : Web Development

After reading Google’s guidelines for multilingual sites, one recommendation stood out that could boost a site’s SEO score:  include the language in the URL. It helps the crawler and the user verify they are looking at content in their preferred language. Additionally, it allows localised content to be referenced by static URLs. This is important for the crawler. Consider the case where you use cookies to store the preferred user’s language. The crawler might not use cookies and therefore never index localised content. Or it could use cookies and get confused as the same URL generates different content. Another recommendation is that duplicate content should be avoided. That is, if the same content can be accessed by 2 URLs, select one as the main URL and redirect the other.

So with these recommendations in mind, here is how I want my URLs on the multilingual site to behave.

  • The home page in default language (English) is the domain root http://www.mysite.com
  • The localised home pages will include the culture code as the first part of its path (http://www.mysite.com/fr/ for French)
  • Generated URLs for the English homepage will never include /en as part of the URL http://www.mysite.com
  • All other localised pages will include the language as the first part of the URL path, including English (http://www.mysite.com/en/cart/view)
  • Disallowed languages will generate a 404 error.

The MVC routing engine will lend itself so easily to meet most of these requirements. Here are the route:

routes.MapRoute(

                name: "SpecificCulture",

                url: "{culture}/{controller}/{action}/{id}",

                defaults: new {

                    controller = "Home",

                    action = "Index",

                    culture = Localisation.DefaultCulture.CultureCode,

                    id = UrlParameter.Optional

                }

            ); 

This route satisfies the first 4 requirements. To test this let’s consider this scenario. A user is currently viewing www.mysite.com/fr/home/about which contains a link to the home page. We generate this link in the view using:

@Url.Action("Index")

Which generates www.mysite.com/fr. This is correct. That same page in English (www.mysite.com/en/home/about) will generate the link as www.mysite.com because there is a default value for culture and routing engine will reduce the URL to its smallest form using the default set.

The next requirement is to generate a 404 error when the user attempts to visit a non-registered language (a language that is not enabled at the database level). To achieve this, I’ve applied the use of filters. 

    public class CultureFilterAttribute : ActionFilterAttribute

    {

        public override void OnActionExecuting(ActionExecutingContext filterContext)

        {

            base.OnActionExecuting(filterContext);

 

            //User selected culture is derived from the culture parameter in the URL

            //If there is no culture parameter, then the page doesn't have culture specific info, and the default will be set.

            var cultureName = (filterContext.RouteData.Values["culture"] as string) ?? string.Empty;

            if (string.IsNullOrWhiteSpace(cultureName))

                return;

           

            if (Localisation.SetThreadCulture(cultureName.ToLower()))

                return;

 

            filterContext.Result = new HttpNotFoundResult();

            throw new HttpException(404, "Page not found");

        }

    } 

This filter sets the CurrentCulture on the thread from the culture value in the route dictionary, if it cannot find that culture, it will throw the 404 error. For allowed languages that have missing translations, I default their contents to English. However, we want to generate a 404 error if the language is not allowed, because we don’t want the user or the crawler to think we support German (for instance) when we actually don’t.

Notice that the filter is an attribute, so you can apply it to controllers or their individual actions and this code will be executed each time before the action is called. For this project, I’ve applied it globally to every request by registering it on application startup in a similar way to routes but in the global filter collection. 

        public static void RegisterGlobalFilters(GlobalFilterCollection filters)

        {

            filters.Add(new CultureFilterAttribute());

        }

There you have it. A multilingual site that has SEO friendly URLs.