I'm creating an ASP.net MVC / Entity Framework shopping cart to get more familiar with the technology. One of the features I wanted to have was URLs based of off unique slugs instead of embedding the entity ids into the URL. Some examples:
- /
- /information
- /information/about-us
- /information/contact-us
- /mens-clothing
- /mens-clothing/mens-shirts
- /mens-clothing/mens-shirts/test-tshirt
The slugs are unique across all content types, but for example, test-tshirt could appear in multiple categories:
- /mens-clothing/mens-shirts/test-tshirt
- /mens-clothing/clearance/test-tshirt
I created a custom route that takes the last slug in the path and uses it to look up the current page.
public class SlugRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string path = HttpContext.Current.Request.Path.TrimStart('/').TrimEnd('/');
if (string.IsNullOrEmpty(path))
path = "home";
string[] slugs = path.Split('/');
string slug = slugs[slugs.Length - 1];
CatalogPage page = Token.Instance.DB.Pages.SingleOrDefault(p => p.UrlSlug == slug);
if (page != null)
{
// Cache current page in context
HttpContext.Current.Items["CurrentPage"] = page;
// Set up route data
RouteData data = new RouteData(this, new MvcRouteHandler());
data.Values["action"] = "Index";
data.Values["id"] = page.Id;
data.DataTokens.Add("namespaces", new string[] { "MyProject.Presentation.Controllers" });
// Set controller value if specified in db, or set based on entity type
if (!string.IsNullOrEmpty(page.Controller))
data.Values["controller"] = page.Controller;
else if (page.GetUnproxiedType() == typeof(CategoryPage))
data.Values["controller"] = "Category";
else if (page.GetUnproxiedType() == typeof(ProductPage))
data.Values["controller"] = "Product";
else
data.Values["controller"] = "Content";
return data;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
This works well and I have good flexibility in implementing custom logic and display templates (the content types have a "View" property as well so I can dynamically set the view in the controller).
However, I stumble a little bit when it comes to implementing breadcrumbs. The quick and dirty way would be to use the path from the URL, do a query for each slug in the path, and ignore whether or not the page is actually a child of the category. Another solution would be to use something like MvcSiteMapProvider and build up an XML tree as content is added on the backend... I'm unsure how well this specific implementation will work because it seems to be pretty focused on the standard {controller}/{action}/{id} route pattern.
What other types of implementations have you used or seen?
MvcSiteMapProvider v4 also works with URLs by setting the Url property rather than using {controller}/{action}/{id}. This is exactly the scenario I am using it for (database driven URLs/custom RoutBase derived routes) and it works great. However, you should implement reverse URL lookup in your route as well or your URL resolution won't work.
I use caching to load all of the URLs into a data structure (my final application will probably use file caching), so the database is not hit for every URL lookup.
MvcSiteMapProvider is also set up to use multiple paths to a single page by creating multiple nodes to the page (one for each unique URL). You can fix the SEO aspect of using multiple URLs for the same content by implementing the canonical tag using the CanonicalUrl or CanonicalKey properties. See this article for a complete example.
You can also drive MvcSiteMapProvider nodes from a database by implementing IDynamicNodeProvider or ISiteMapNodeProvider.
Do note that the URL matching in MvcSiteMapProvider is case sensitive. It would be best if you ensure your incoming URLs are always lowercase by doing a 301 redirect.