Software Engineering and Architecture

.NET MVC SEO/Slug Only URLs for eCommerce

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

Instead of:

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:

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 t(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)


                    ' 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


                ' 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)

            New With {.action = "Index",
                      .id = UrlParameter.Optional}

        ' Below now handles all that didnt match as a clean SEO URL
        routes.MapRoute("SEO Slug URLs",
            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()



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

        'TODO: Below is temp for testing

    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:

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.