.NET MVC SEO/Slug Only URLs for eCommerce

19 Feb 2011

As part of my ongoing journey creating an eCommerce site I had to implement into the .NET MVC 3 website I’m creating a way of having URLs such as

website.com/pretty-pink-boots

Instead of:

website.com/product/view/100/pretty-pink-boots

The theory being that the URL is shorter and more SEO friendly, as it contains just the important information and the full URL is shown in Google. The keen eyed amongst you will realize that this means we have no “unique ID” or controller specified in the URL - so how do we know whether it’s a product and which product to route to? Possible solutions could be:

  • Have a route at the bottom of the list of routes of simply {id} and route it to the ProductController. The problem here is that the slug is not the unique identifier for a product so I would have to change the product ID in the website to ‘pretty-pink-boots’ or have two unique IDs. The other problem is that we could only have the slug only URL to products, what about categories and content pages?
  • Have a route at the bottom of the list of routes of simply {id} again but this time route towards an SEOController. This controller would then do a RedirectToAction to the correct controller (Product or Category). Here we now have the ability to have slug only URLs for different areas of the site but the redirect causes a HTTP redirect and we definitely don’t want that from an SEO point of view. We still have the unique ID problem.

So my solution was as follows:

  1. Create a controller factory that inherits from the DefaultControllerFactory
  2. Override the GetControllerInstance function and put in some logic that if the controller requested is of type SEOController we will change the route data to (for example) “/Product/View/100” and return that controller instance.
  3. Tell MVC to use the new controller
  4. Build in a URLManager that takes in the slug and returns whether it’s a valid URL, and if so, the controller type and unique ID
  5. If no slug exists for the requested URL then a 404 must be returned
  6. Finally a route needs setting up for the SEOController, but the route must be as strict as possible and not “take over” from other URLs.

So here’s the code. First the Controller Factory

Imports makit.WebUI.Controllers
Imports System.Globalization
Imports makit.Core.URLs

Namespace Infrastructure

    Public Class MakitControllerFactory
        Inherits DefaultControllerFactory

        Protected Overrides Function GetControllerInstance(ByVal requestContext As RequestContext, ByVal controllerType As Type) As IController

            If controllerType Is Nothing Then
                Throw New HttpException(404, String.Format("The controller for path '{0}' was not found or does not implement IController", requestContext.HttpContext.Request.Path))
            End If

            If Not GetType(IController).IsAssignableFrom(controllerType) Then
                Throw New ArgumentException(String.Format("The controller type '{0}' must implement IController.", controllerType), "controllerType")
            End If

            ' This code is the custom bit to handle slug only URLs
            If controllerType Is GetType(SEOController) Then

                ' Check if it is an actual slug and get the product/category if it is
                Dim slug As URLManager.SlugRewrite = URLManager.getURLSlugRewrite(requestContext.RouteData.Values("id").ToString)

                If slug.exists Then

                    ' Rewrite the routedata to point to the actual controller and id
                    requestContext.RouteData.Values("controller") = slug.pageType
                    requestContext.RouteData.Values("action") = "Display"
                    requestContext.RouteData.Values("id") = slug.pageID

                    Select Case slug.pageType
                        Case "Category"
                            controllerType = GetType(CategoryController)
                        Case "ContentPage"
                            controllerType = GetType(ContentPageController)
                        Case "Product"
                            controllerType = GetType(ProductController)
                    End Select

                    Return MyBase.GetControllerInstance(requestContext, controllerType)

                Else

                    ' A normal 404 because not a slug url
                    Throw New HttpException(404, String.Format("No slug exists for '{0}'", requestContext.HttpContext.Request.Path))

                End If

            Else

                ' Not slug only so default to the base method
                Return MyBase.GetControllerInstance(requestContext, controllerType)

            End If

        End Function

    End Class

End Namespace
The SEOController just needs to exist:
Namespace Controllers

    Public Class SEOController
        Inherits System.Web.Mvc.Controller

    End Class

End Namespace
Global.asax changes:
Imports makit.Core.Infrastructure
Imports makit.WebUI.Infrastructure

Public Class MvcApplication
    Inherits System.Web.HttpApplication

    Shared Sub RegisterGlobalFilters(ByVal filters As GlobalFilterCollection)
        filters.Add(New HandleErrorAttribute())
    End Sub

    Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

        routes.MapRoute("Default",
            "{controller}/{action}/{id}",
            New With {.action = "Index",
                      .id = UrlParameter.Optional}
        )

        ' Below now handles all that didnt match as a clean SEO URL
        routes.MapRoute("SEO Slug URLs",
            "{id}",
            New With {.controller = "SEO", .action = "Index"},
            New With {.id = "^[A-Za-z0-9\-]+$"}
        )

        ' Below now handles all that didnt match as a clean SEO URL
        routes.MapRoute("Home Page",
            "",
            New With {.controller = "Home", .action = "Index"}
        )

    End Sub

    Sub Application_Start()

        AreaRegistration.RegisterAllAreas()
        RegisterGlobalFilters(GlobalFilters.Filters)

        RegisterRoutes(RouteTable.Routes)

        ' Custom factory which handles slug only URLs
        ControllerBuilder.Current.SetControllerFactory(New MakitControllerFactory())

        'TODO: Below is temp for testing
        'RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes)

    End Sub

End Class
Finally the URLManager, here is just an example stub, this will most likely be programmed with a Dictionary object in memory containing all the slugs as keys and the SlugRewrite structures as the values
Namespace URLs

    Public Class URLManager

        Public Structure SlugRewrite
            Dim exists As Boolean
            Dim pageType As String
            Dim pageID As String
        End Structure

        Public Shared Function getURLSlugRewrite(ByVal slug As String) As SlugRewrite

            Dim slugReturn As New SlugRewrite

            Select Case slug
                Case "pencils"
                    slugReturn.exists = True
                    slugReturn.pageType = "Category"
                    slugReturn.pageID = "35"
                Case "pretty-pink-boots"
                    slugReturn.exists = True
                    slugReturn.pageType = "Product"
                    slugReturn.pageID = "2936"
            End Select

            Return slugReturn

        End Function

    End Class

End Namespace

Several things to note:

  • When building the site you will also need a reverse lookup to the slugs, in my head I will be having a slug table in the DB like the following:

    Slug - “pencils”
    Type - “Category”
    ID “35”

    But also have the slug in the product table as a column so we have a lookup from both ways.
  • The links in the site will then need to link through to the page using the slug if one exists, which can now be gotten from the product table. If no slug exists it can then default to the Product/View/100 style URL.
  • Finally, the routing I’ve done will only route with URLs not matching any other route and a URL containing only alpha numeric characters and a dash - even so the URLManager could get a lot of hits if spiders are trying lots of types of URLs so the slug checking needs to be cached and high performance - no straight to DB calls.
mvc   asp.net   seo  
Posted by makit
Last revised 18 May 2012 12:00 PM

makit / Martyn Kilbryde

Professional software developer, ponderer and eccentric.

flickr

Github

LinkedIn

Stack Overflow

Twitter