ASP.NET MVC and Clean SEO Friendly URLs

18 March 2009 - 1:30 AM / by Dominic Pettifer. 15 Comments

Technical Article - I show you how to generate SEO friendly URLs in ASP.NET MVC using the actual title/name of the record or item (product/blog name etc.) instead of the ID. That way we can have nice looking URLs that look like /products/123/apple-ipod-nano-4gb instead of /products/detail/123. The trouble is by default, ASP.NET MVC Html helpers don’t always encode the URL properly. Also how do we deal with updating the title, and old broken links?

SEO Friendly URLs

Believe it or not, search engines such as Google will pay attention to keywords contained in the URL, so that a URL like gadgetshop.com/products/123/apple-ipod-nano-4gb will rank higher in the search engines than a URL that looks like gadgetshop.com/showProduct.aspx?id=123. Also if people post links to your site in message boards/blogs/MyFace pages etc. they tend to paste the URL in directly. Other people will see a hyperlink with the text http://www.gadgetshop.com/products/123/apple-ipod-nano-4gb and can easily imagine where the page might lead to (a page to buy a 4GB iPod Nano), whereas the URL http://www.gadgetshop.com/showProduct.aspx?id=123 doesn't give any hint as to where it might lead to. So, nice clean URLs are clearly beneficial.

One of the major benefits of ASP.NET MVC is that it allows us truly dynamic URLs via its Routing system. We could declare a new Route like this:

routes.MapRoute(
    "ViewProduct",
    "products/{id}/{productName}",
    new { controller = "Product", action = "Detail", id = "", productName = "" }
);

...and in your ProductController class...

public ActionResult Detail(int? id, string productName)
{
    Product product = IProductRepository.Fetch(id);
    return View(product);
}

Plenty of tutorials on ASP.NET MVC declare routes for detail pages as /Product/View/{id} which doesn't include any useful SEO indexable content in the URL. So I thought for this blog, I would declare the View Blog Route as /Blogs/{id}/{blogTitle} (Note: I'm currently rewriting this blog software in MVC).

URL Encoding Problems

Things were fine until I hit a snag with some of the titles for my blogs that contained funny characters. For instance, "JavaScript/CSS and Custom Controls" (the slash causes MVC routing system to think it's a folder), and "Get a<HEAD> with ASP.NET 2.0" (HTML brackets) were causing problems.

I was doing things properly and using Html.ActionLink helper method:

<%= Html.ActionLink(item.Title, "View", "Blog",
    new { id = item.BlogId, blogTitle = item.Title }, null)%>

...but it just wasn't encoding the special characters properly. Limitation with ASP.NET MVC? I'm not sure, someone please enlighten me!

Solution

I wrote a simple method that encodes the URLs into a nice, safe, SEO friendly URL format:

public static class UrlEncoder
{
    public static string ToFriendlyUrl (this UrlHelper helper,
        string urlToEncode)
    {
        urlToEncode = (urlToEncode ?? "").Trim().ToLower();

        StringBuilder url = new StringBuilder();

        foreach (char ch in urlToEncode)
        {
            switch (ch)
            {
                case ' ':
                    url.Append('-');
                    break;
                case '&':
                    url.Append("and");
                    break;
                case '\'':
                    break;
                default:
                    if ((ch >= '0' && ch <= '9') ||
                        (ch >= 'a' && ch <= 'z'))
                    {
                        url.Append(ch);
                    }
                    else
                    {
                        url.Append('-');
                    }
                    break;
            }
        }

        return url.ToString();
    }
}

Notice the "(this UrlHelper helper," part? This is an extension method so we can access it in our view using:

<%= Url.ToFriendlyUrl(item.Title) %>

It works on a white-list principle, allowing all 0-9 and a-z characters through, dealing with a few special cases, and converting everything else to -hyphens-. You'll notice that spaces are being converted to hyphens as well, and you might be tempted to use _underscores_ instead. Don't! There are sound SEO benefits for using hyphens in that search engines bots treat them as spaces. "Get a<HEAD> with ASP.NET 2.0" although it gets converted to "get-a-head--with-asp-net-2-0", search bots will treat it as "get a head with asp net 2 0", providing some nice indexable keyword content.

Now we can change our HTML ActionLink helper method to:

<a href="<%= Url.Action("View", "Blog",
   new { id = item.BlogId, blogTitle = Url.ToFriendlyUrl(item.Title) }) %>">
   <%= Html.Encode(item.Title) %>
</a>

Why keep the ID in the URL?

You might be wondering why we carry the id in the URL, why not just have the URL as /Blogs/get-a-head--with-asp-net-2-0. Well first, when we encode the blog title in the URL, we lose a bit of information. It would be difficult to match "get-a-head--with-asp-net-2-0" to the actual title "Get a<HEAD> with ASP.NET 2.0" stored in the database. We could easily overcome this by storing the fully encoded URL in the database as a separate field and search on that. That would certainly work, except if the URLEncode function was later updated and database values weren't synced up properly to the new way the function was encoding URLs.

But one of the biggest reasons is performance. Lookups performed on Primary Key IDs in databases are a lot faster than searching on a VARCHAR field.

Updating the URL Title

We also have the benefit that we can tell search engines if the blog (or indeed product) title changes. If we have the URL /Products/123/ipod-nano, Google Bot goes about indexing the site and puts the page /Products/123/ipod-nano into its index, and users add the link to forums, theirs blogs and so on. Later the administrator updates the product title to something a little more descriptive so the URL is now /Products/123/apple-ipod-nano-4gb-black-3rd-generation.

But wait, we now have a bunch of old URLs all over the place, in blogs/forums etc. and on Google's index that point to the old page /Products/123/ipod-nano. Broken links!? Not at all, because the ID '123' is still the same, Primary Key IDs never change, and this is what the database lookup is performed on, so it can still display the correct product, no need for any 404. We can programmatically detect if the product title URL is different from the one stored in the database, and issue a 301 response if it is.

A 301 response code is a 'Permanent Redirect' response (another response code similar to 200 OK, or 404 Page Not Found), it basically tells Google that the page has 'permanently' moved to /Products/123/apple-ipod-nano-4gb-black-3rd-generation if it tries to re-index /Products/123/ipod-nano again, and Google will update its index accordingly. Note that calling Response.Redirect(); in code, issues a 302 response code, which is a 'temporary' redirect (page has been temporarily moved), these are no good as google won't follow them.

Finally, users clicking on a /Products/123/ipod-nano link in a blog or bulletin board will be harmlessly redirected to the correct URL, so when they try to bookmark, or copy/paste the URL from the address bar themselves, it'll be the correct one. Here is some code on how you how could do this from your Controller Action:

public ActionResult Details(int? id, string productTitle)
{
    Product product = ProductRepository.Fetch(id);

    string realTitle = UrlEncoder.ToFriendlyUrl(product.Title);
    string urlTitle = (productTitle ?? "").Trim().ToLower();

    if (realTitle != urlTitle)
    {
        Response.Status = "301 Moved Permanently";
        Response.StatusCode = 301;
        Response.AddHeader("Location", "/Products/" + product.Id + "/" + realTitle);
        Response.End();
    }

    return View(product);
}

15 Comments on "ASP.NET MVC, SEO & URLs"

Post a Comment
  • Re:

    I will recommend not to hold back until you earn enough cash to order goods! You can just get the <a href="http://bestfinance-blog.com">loans</a> or just student loan and feel fine

    Posted on 30 July 2010 - 8:48 PM / by KinneyJudy

  • RE: ASP.NET MVC, SEO & URLs

    Great, been looking for something like this. But, say you want to go TRULY dynamic and store your controller > url mappings (with the rest of the menu info) in your database, which you then obviously generate on every pagerequest...any idea how you could accomplish that?

    Posted on 21 January 2010 - 2:42 PM / by Pleun

  • RE: ASP.NET MVC, SEO & URLs

    this is great I altered only the view code to show:

    <a href="<%= Url.Action("post", "blog", 
       new { id = BlogPost.ID, Title = Url.ToFriendlyUrl(BlogPost.Title) }) %>

    but when I do it renders a ?Title= instead of a / in the url as shown here:

    blog/post/4?Title=blog-post-title-4

    Posted on 25 December 2009 - 12:21 AM / by John

  • RE: ASP.NET MVC, SEO & URLs

    Great Post. Just what I needed. Here is a complete sample of a class you could use - basically a combination of the above techniques.

    public static class UrlEncoderHelper
    {
    public static string ToFriendlyUrl( this UrlHelper helper, string urlToEncode )
    {
    urlToEncode = (urlToEncode ?? "").Trim().ToLower();
    urlToEncode = Regex.Replace( urlToEncode, @"[^a-z0-9]", "-" ); // invalid chars
    urlToEncode = Regex.Replace( urlToEncode, @"-+", "-" ).Trim(); // convert multiple dashes into one
    return urlToEncode;
    }
    }

    Posted on 29 October 2009 - 5:38 PM / by Stuart Clode

  • RE: ASP.NET MVC, SEO & URLs

    Hi All

    I have never posted a comment before so this is new for me.

    Thanks for the article Dominic. I have created a little URL helper which gives a nice clean url string. Here it is

    public static string CleanURL(this UrlHelper helper, string originalUrl)
    {
    string cleanUrl = Regex.Replace(originalUrl, @"[^a-z0-9\s-]", "_"); // invalid chars
    cleanUrl = Regex.Replace(originalUrl, @"\s+", "_").Trim(); // convert multiple spaces into one
    return cleanUrl;
    }

    To use it, you must use the System.Text.RegularExpressions namespace. Hope that helps someone else.

    Rob

    Posted on 1 October 2009 - 1:39 PM / by Rob

  • RE: ASP.NET MVC, SEO & URLs

    Instead of a huge function uo could do this with one line of code, using a simple regular expression pattern based on something like this:

    return new Regex("[^a-zA-Z]").Replace(s, "_")

    I.e. if a character isn't acceptable, replace it with one that is.

    I would always try to avoid loops in managed code, especially in a web application where performance is naturally an important consideration. I would also try to avoid string manipulation in managed code.

    The System.Text namespace in the .NET framework is an unmanaged library that provides string manipulation classes like Regex which will typically perform better and at a lower level than anything we would write in C# or VB.NET.

    Posted on 3 July 2009 - 2:32 PM / by Tim Acheson

  • RE: ASP.NET MVC, SEO & URLs

    @Nik,

    You will need to add reference to the Namespace in which your URLEncoder class is onto the viewpage.

    For Example:
    <%@Import Namespace="MvcApplication1.Helpers" %>

    Your <%= Html.ToFriendlyUrl()%> should be available in all other places on the page.

    Posted on 12 June 2009 - 5:19 PM / by Turnkey

  • RE: ASP.NET MVC, SEO & URLs

    Good post Dominic!

    Where did you put the UrlEncoder class? My view can't see the method.

    A slight problem with your method is that it could potentially lead to duplicate content in search engines.

    e.g.

    If multiple URLs with the same ID in the URL get linked to from somewhere on the web and indexed by a search engine they effectively link to the same page, hence duplicate content.

    http://www.dominicpettifer.co.uk/Blog/34/asp-net-mvc-and-clean-seo-friendly-urls

    http://www.dominicpettifer.co.uk/Blog/34/asp-net-mvc-and-clean-seo-friendly-urls-some-other-text-here

    Both the URLs above link to this page...

    Posted on 12 June 2009 - 5:13 PM / by Nik Makris

    • Old URL Would Redirect to New one

      Nik Makris says...

      If multiple URLs with the same ID in the URL get linked to from somewhere on the web and indexed by a search engine they effectively link to the same page, hence duplicate content.

      http://www.dominicpettifer.co.uk/Blog/34/asp-net-mvc-and-clean-seo-friendly-urls

      http://www.dominicpettifer.co.uk/Blog/34/asp-net-mvc-and-clean-seo-friendly-urls-some-other-text-here

      Both the URLs above link to this page...

      The old out-of-date URL would 301 redirect to the new URL. Although both URLs could potentially exist in the search engine's database, the search engine would eventually reindex the links, find the 301, and remove the old URL from it's index.

      Posted on 12 June 2009 - 7:21 PM / by Dominic Pettifer (Administrator)

  • RE: ASP.NET MVC, SEO & URLs

    Cool! Thanks for your suggestion.

    Have you by any chance blogged about ASP.NET MVC Client and server validation? I'm fighting away with it. I was following S.Hanselmann's post about NerdDinner to get a good idea about MVC (mainly because I was interested in finding out Business rule and data validation integration), however, got stuck at Validation!! Scott is using Linq to SQL but I'm using Entity Framework as DAL. There is no OnValidate Event in EntityFramework! Got directed towards a MSDN article about entity framework validation but it is painfully difficult and clumsy to get it to work. What I want is a clean and easy client and server validation. Something that generates JQuery javascript by interrogating the model that is in context for that particular view. This model could be an Entity object with DataAnnotation validation attributes added to all the properties that need validation.

    Perhaps there is a better way in doing what I want to achieve?? Any thoughts are much appreciated.

    P.S. Couldn't post my code here, may be too large!

    Posted on 28 May 2009 - 11:21 AM / by Turnkey

  • RE: ASP.NET MVC, SEO & URLs

    I have been constructing SEO friendly URLs myself but did not care about the special characters very much. Perhaps it is a good idea to take those in consideration.

    Also, I haven't really developed websites for the Internet but good to know how google bots work. Dominic mentioned that if a product's title has changed we could respond to the request by a StatusCode of 301. What if the product is discontinued or no longer available? What is the response status code then? What would you recommend to do in that case? Still show a page that dictates that the product is no longer available?

    There might be no harm in showing the page but if it is no longer relevant perhaps we should inform that that product no longer exists. In my case, I am dealing with a property website, I don't want to keep/show properties that have been SOLD. If someone keeps the URL indexed/bookmarked and I do not want to show the page if the property is sold, is there a gracious way to handle this?

    Posted on 25 May 2009 - 10:03 AM / by Turnkey

    • Try a 404

      Turnkey says... What if the product is discontinued or no longer available? What is the response status code then? What would you recommend to do in that case? Still show a page that dictates that the product is no longer available?

      ...I don't want to keep/show properties that have been SOLD. If someone keeps the URL indexed/bookmarked and I do not want to show the page if the property is sold, is there a gracious way to handle this?

      You could issue a custom built 404 page with a "call to action" eg. "The property you are looking for is no longer available but you might be interested in these properties instead" or something to that effect. You could even check the property title string for clues (/Properties/123/2-bedroom-flat-in-manchester) and perform a search on that and display related properties on the 404 page.

      The fact you're serving a 404 will mean Google will remove the page from it's index (providing you've removed inbound links on your site to that page), while any users with the URL bookmarked, or following from another site, will get a useful list of related Properties they might be interested in.

      Posted on 28 May 2009 - 10:58 AM / by Dominic Pettifer (Administrator)

  • RE: ASP.NET MVC, SEO & URLs

    Nice article. Stupid that the standard Url.Encode and HttpUtility.UrlEncod() don't catch the forward slash.

    Posted on 20 March 2009 - 9:05 AM / by BorisCallens

Leave a Comment

Comment Details
*
* BBCode: [b]bold[/b], [i]italics[/i], [code]code[/code], [li]bullet point[/li], [h]Heading[/h], [url="http://www.example.com"]link[/url], [quote author="John Smith"]quote[/quote]

Random Image

14.35% Silverlight 2, 5.26% Silverlight 1, 80.38% No Silverlight

Silverlight Usage Statistics from Google Analytics. (from the blog Add Silverlight Tracking to your Google Analytics )

Quick Poll

What is your DIP/IOC Container of choice?

Poll Vote
(see results)
View Comments (0) (See previous polls)

Latest Tweets

  • Red Bull gives you wings....that generate huge amounts of downforce #F1

    about 18 hours ago from Twitterrific
  • .vampire { -webkit-box-shadow: none; -webkit-box-reflection: none; } #cssjokes

    7:44 PM July 30th from Echofon
  • @edhenderson lol, lets get a trending topic going - .gangster .wrapper { color: #000; width: 150%; text-decoration: bling; } #cssjokes

    7:36 PM July 30th from Echofon
  • @weblivz I think the petition should be resubmitted but with security stuff taken out, as that's what the response purely focused on

    6:13 PM July 30th from Echofon
  • @weblivz I still think Chrome Frame can come to the rescue here, still keep their old browsers + legacy systems, no retraining costs etc.

    6:12 PM July 30th from Echofon

View Dominic Pettifer's Twitter page.