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:
So my solution was as follows:
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:
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.