If you are a Sitecore CMS developer and you love shopping on Amazon, you probably had the same thought: what would it take to re-create Amazon.com in Sitecore? Completely from scratch. Ok, maybe it’s just me.
So you will need to think about how to architect your content tree properly. Luckily, Derek has a blog dedicated solely to content tree architecture so I don’t need to go into much detail here.
Let’s say you have your content tree already designed. We are going to take Nicam demo site as an example:
So all the camera products are structured in categories, that’s nice.
Since we talk about URLs in this post, everything is set there too. Each product has a content path which more or less serves as product URL on the public facing side.
In other words, I would be able to access my D3X camera using the following URL:
http://localhost/en/Products/Digital_SLR/Full_featured/D3X.aspx
One of the things you need to know is that Sitecore is constructing those URLs on the fly based on LinkManager configuration in web.config. And it is actually doing a pretty great job out of the box by giving you a number of options when it comes to URL construction. You can prepend language ISO code in URL, use display name instead of item name, etc. You can learn more about it here.
But what if we want to have fluid URLs that are constructed based on product attributes, meta data or some other criteria? Just as on Amazon.com, where the product URL is clearly driven by product attributes:
For example, for all of our SLR cameras, use the following URL pattern: http://localhost/{SLR}/{Name}/{SKU}, where SKU is an attribute on the product itself:
Well, as it turns out, it is quite straightforward to do.
When it comes to handling any custom URL handling requirements, there are mainly two components you have to deal with.
1. Custom Item Resolver.
The custom logic here will attempt to resolve a valid item in the content tree by the custom URL.
2. Custom Link Provider.
This is the flip side of the solution. We need to teach Sitecore to generate product URLs based on our custom rules.
In addition, you would generally need a component to process such custom URL rules. In my example I would simply use IDTable, which allows to store any mapping to an item in a flat table. For the sake of simplicity I will be updating my IDTable based mapping table every time an item is saved via a handler.
The result will look something like this:

So here are all the pieces:
1. Custom Item Resolver:
public class ProductUrlResolver : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
Assert.ArgumentNotNull(args, "args");
if (Context.Item != null || Context.Database == null || args.Url.ItemPath.Length == 0) return;
Context.Item = ProductUrlManager.GetProductItemByFilePath(args.Url.FilePath);
}
}
<processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel"/>
<!-- the proper order is important -->
<processor type="Custom.ProductUrlResolver, ProductUrlResolver"/>
<processor type="Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel"/>
2. Custom Link Provider:
public class ProductLinkProvider : LinkProvider
{
public override string GetItemUrl(Item item, UrlOptions options)
{
Assert.ArgumentNotNull(item, "item");
Assert.ArgumentNotNull(options, "options");
return item.IsProduct() ? item.ProductUrl() : base.GetItemUrl(item, options);
}
}
<linkManager defaultProvider="sitecore">
<providers>
<clear/>
<add name="sitecore"
type="Custom.ProductLinkProvider, ProductUrlResolver"
.../>
</providers>
</linkManager>
3. ItemSaved event handler:
public class ProductHandler
{
protected void OnItemSaved(object sender, EventArgs args)
{
if (args == null) return;
var item = Event.ExtractParameter(args, 0) as Item;
if (item == null) return;
if (item.IsProduct())
{
item.RegisterMapping();
}
}
}
<event name="item:saved">
...
<handler type="Custom.ProductHandler,ProductUrlResolver" method="OnItemSaved"/>
</event>
4. Utility Manager where all the logic is handled:
public static class ProductUrlManager
{
public static string IdTableKey
{
get { return "ProductResolver"; }
}
public static bool IsProduct(this Item item)
{
var template = TemplateManager.GetTemplate(item);
return template != null &&
template.DescendsFromOrEquals(ID.Parse("{B87EFAE7-D3D5-4E07-A6FC-012AAA13A6CF}"));
}
public static string ProductUrl(this Item item)
{
return "/{0}/{1}/{2}".FormatWith(item.TemplateName.ToLowerInvariant(),
item.Name.ToLowerInvariant(),
item["SKU"]);
}
public static Item GetProductItemByFilePath(string filePath)
{
var id = IDTable.GetID(ProductUrlManager.IdTableKey, filePath);
if (id != null && !ID.IsNullOrEmpty(id.ID))
{
return Context.Database.GetItem(id.ID);
}
return null;
}
public static void RegisterMapping(this Item item)
{
IDTable.RemoveID(ProductUrlManager.IdTableKey, item.ID);
IDTable.Add(ProductUrlManager.IdTableKey, item.ProductUrl(), item.ID);
}
}
Conceptually, what do you think about this?
Note that this is a pretty hardcoded way of implementing such requirement. Please consider this as a prototype rather than a solution ready for production.