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:
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.
3 comments:
Interesting stuff! Would the CustomItemResolver be a good way of handling URL aliasing?
It's one approach, but it does seem like a lot of work. Part of your URL will always be some sort of unique product identifier, and I would probably just go looking for this just before the Item Resolver attempts it in the request pipeline.
Something like
1) Split the URL up in its component parts, to find an ASIN. If found, continue.
2) Ask Lucene if the ASIN exists
3) If so, resolve the item in the pipeline to what Lucene found
Saves you the extra trouble of creating save handlers etc, assuming of course that you have a current lucene index on your solution. And for a webshop, you always would have.
Hi Mark,
Thanks for the feedback guys.
Mark:
I would not agree that it is a lot of work, the solution is actually very simple. The save handler - I used it only for the sake of simplicity. In reality, I'd leverage something else. Maybe a workflow action that makes sure that the mapping table is updated every time a version is published.
I would not use Lucene simply because in two server setups the front-end index is not updated immediately, and URL is a very critical component.
Mapping table seems like the only bulletproof and quite lightweight way to go. How you populate it is another question (item:saved, wf action or publish:item processor).
I would also not put a lot of additional string processing on this side as both CustomItemResolver and LinkManager are used quite frequently on the delivery side. Reading from a mapping table is easy, plus you can cache it in memory. Messing with strings and querying Lucene every time an custom item is being resolved or a link is being generated could be more expensive.
Post a Comment