Item Buckets and URLs
Update: Please also read http://blog.horizontalintegration.com/2013/07/30/sitecore-contentsearch-fails-for-lucene-reserved-keywords-like-andor/ to solve issues if you have common stopwords in your item names such as "and" and "or".
So it seems everyone is having fun with Item Buckets in Sitecore 7. No seriously, go take a look at the social channels. Item Buckets open a realm of new architectural design options. One point of consideration when using Item Buckets is URLs. Let's take a look at the default URL behavior of items in a bucket.
I've setup an item bucket and created some items in it. Now if I go to the home item and edit the text, inserting a link to an item in the bucket, take a look at the URL:
http:/Products/2013/07/14/22/01/dolor.aspx
Notice how the structure of the bucket is reflected in the URL, which is normal Sitecore behavior; the URL is based on the name of the item and it's ancestors in the content tree. The default bucket structuring algorithm uses the item creation date to structure the items within the bucket. There are a few settings we have access to to control the structuring.
Firstly, there is the BucketConfiguration.BucketFolderPath
Sitecore setting defined in the App_config\Include\Sitecore.Buckets.config
file. This setting allows defining the structure of the folders within the bucket using aspects of the creation date such as year, month and day. But if storing items by date (and hence generating URLs based on the date) doesn't suit, one can also change the class used to create the bucket folder structure. The class used is defined in the BucketConfiguration.DynamicBucketFolderPath
Sitecore setting (also defined in the Sitecore.Buckets.config
patch config file above).
Structuring items by creation date may be fine for certain kinds of content such as blog posts and some types of general article (thought leadership pieces), but this structure is not appropriate for all content. Take for example a product catalog, where the date the product item was created would be irrelevant to the context. We could change the dynamic bucket folder path class as mentioned above, but instead I'd prefer to completely hide the bucket implementation from the world. After all, the whole idea of an item bucket was to remove the detail of the structuring of the item in the content tree and treat content within the bucket as a pool of content instead. Not to mention the fact that I don't want to have to address URL continuity of items if I needed to change the bucket structure later on.
So instead, I'm going to implement a solution which will remove all item bucket folders from the URL. To achieve this I'll need to add a processor to the httpRequestBegin
pipeline right after the out-of-the-box ItemResolver
so items within the bucket can be requested without the folder structure within the bucket. I'll also need to replace the LinkProvider
so links to items in a bucket don't include the bucket folders as well. Let's get started on the ItemResolver
.
using System;
using System.Linq;
using Sitecore;
using Sitecore.Buckets.Managers;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Pipelines.HttpRequest;
namespace CustomBucketUrl
{
public class CustomItemResolver : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
if (Context.Item == null)
{
var requestUrl = args.Url.ItemPath;
// remove last element from path and see if resulting path
is a bucket
var index = requestUrl.LastIndexOf('/');
if (index > 0)
{
var bucketPath = requestUrl.Substring(0, index);
var bucketItem = args.GetItem(bucketPath);
if (bucketItem != null && BucketManager.IsBucket(bucketItem))
{
var itemName = requestUrl.Substring(index + 1);
// locate item in bucket by name
using (var searchContext =
ContentSearchManager.GetIndex(bucketItem as IIndexable)
.CreateSearchContext())
{
var result = searchContext
.GetQueryable<SearchResultItem>().Where(
x => x.Name == itemName).FirstOrDefault();
if(result != null)
Context.Item = result.GetItem();
}
}
}
}
}
}
}
The above item resolver is assuming the last part of the URL is the item name and everything before that is the URL to the bucket. So we extract the path of the bucket, get the item and verify it's a bucket. Then we extract the item name (last segment of the URL) and use the shiny new content search API to find an item within the bucket matching the item name. Something to note here if you use this technique, you'll need to ensure all items within the bucket have unique names as the code above is matching the first item in the bucket with the given name. Now, onto the custom LinkProvider
which will generate links without the bucket folders.
using System;
using Sitecore.Buckets.Managers;
using Sitecore.Buckets.Extensions;
using Sitecore.Links;
using Sitecore.IO;
namespace CustomBucketUrl
{
public class CustomLinkManager : LinkProvider
{
public override string GetItemUrl(Sitecore.Data.Items.Item item,
UrlOptions options)
{
if (BucketManager.IsItemContainedWithinBucket(item))
{
var bucketItem = item.GetParentBucketItemOrParent();
if (bucketItem != null && bucketItem.IsABucket())
{
var bucketUrl = base.GetItemUrl(bucketItem, options);
if (options.AddAspxExtension)
bucketUrl = bucketUrl.Replace(".aspx", string.Empty);
return FileUtil.MakePath(bucketUrl, item.Name) +
(options.AddAspxExtension ? ".aspx" : string.Empty);
}
}
return base.GetItemUrl(item, options);
}
}
}
The above class first checks to ensure the item is in a bucket, then gets the URL for the bucket itself (without the .aspx
extension if they're being used) and append that path with the name of the item (and adding back the .aspx
extension if they're being used). Now to add the custom classes into Sitecore using a configuration patch file (.config
file in App_Config\Include
).
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<httpRequestBegin>
<processor patch:after="
processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver,
Sitecore.Kernel']"
type="CustomBucketUrl.CustomItemResolver, CustomBucketUrl" />
</httpRequestBegin>
</pipelines>
<linkManager>
<providers>
<add name="sitecore">
<patch:attribute name="type">
CustomBucketUrl.CustomLinkManager, CustomBucketUrl</patch:attribute>
</add>
</providers>
</linkManager>
</sitecore>
</configuration>
Notice how the custom ItemResolver
is added after the default ItemResolver
. This allows our custom resolver to only handle our specific cases when the default resolver cannot locate the item by URL. With the customisations now in place my item bucket URLs just got a lot nicer looking. They have gone from something looking like http://localhost:8080/Products/2013/07/14/22/01/dolor.aspx
to something looking like http://localhost:8080/Products/dolor.aspx
One more point to mention, if you use this technique ensure you handle all the cases in the LinkProvider
such as if the UrlOptions
have been set to use display name or encode the name.
If you have item names with words like "and" or "or" in your item name and the ContentSearch fails, please refer to this blog post: http://blog.horizontalintegration.com/2013/07/30/sitecore-contentsearch-fails-for-lucene-reserved-keywords-like-andor/