.NET MVC SEO/Slug Only URLs for eCommerce
Posted on February 19, 2011 • 5 minutes • 993 words
Table of contents
Introduction
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.
Solution
So my solution was as follows:
- Create a controller factory that inherits from the DefaultControllerFactory
- 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.
- Tell MVC to use the new controller
- 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
- If no slug exists for the requested URL then a 404 must be returned
- 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.
Code
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
To Note
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.