codeflood logo

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://localhost:8080/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.

Comments

bsvachzi

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/

I like your ideas on this topic, though I am worried about content editors having 2 items with the same name for example. How would you deal with a bucket having 1 or more items with the same name? You are searching a bucket using the item name, which means duplicates can cause trouble. Have you thought about this, and how would you deal with it?

Alistair Deneys

Hi Younesvanruth,
Yes, you're right. Duplicate item names within a bucket are a concern. I didn't cover that in my post as I wanted to just explore the idea of the URL resolution and link generation (I put a warning in there about items with the same name in the same bucket). But the way I would handle it (and the topic for another post) would be to use a custom item validator. Item and field validators are part of Sitecore adn we can leverage that functionality. Just create an item validator which uses the same logic to search for an item in the bucket using the current items name, and return an error if any items are found.
Like I said...topic for another post...stay tuned.

Raj

Hello, I followed the above approach however i see that requestUrl is getting set to /notfound and page redirects to /sitecore/service/notfound.aspx .Any idea on why this is happening. Thanks,

Alistair Deneys

Hi Raj, It could be that your pipeline processor is not inserted into the pipeline at the correct location (immediately after the ItemResolver). You can use the /sitecore/admin/showconfig.aspx tool to see the fully patched config and check the order of the processors. If that is the issue, check the URL you've used on the patch namespace. The trailing slash is important and a common reason for patching not working properly.

Vaibhav

Hi,
I followed your methods to create custom classes and adding the configuration files, but I keep getting, Could not resolve type name: CustomBucketUrl.CustomLinkManager, CustomBucketUrl (method: Sitecore.Configuration.Factory.CreateType(XmlNode configNode, String[] parameters, Boolean assert)).
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Exception: Could not resolve type name: CustomBucketUrl.CustomLinkManager, CustomBucketUrl (method: Sitecore.Configuration.Factory.CreateType(XmlNode configNode, String[] parameters, Boolean assert)).
Source Error:
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.
Stack Trace:

[Exception: Could not resolve type name: CustomBucketUrl.CustomLinkManager, CustomBucketUrl (method: Sitecore.Configuration.Factory.CreateType(XmlNode configNode, String[] parameters, Boolean assert)).] Sitecore.Diagnostics.Error.Raise(String error, String method) +129 Sitecore.Configuration.Factory.CreateType(XmlNode configNode, String[] parameters, Boolean assert) +432 Sitecore.Configuration.Factory.CreateFromTypeName(XmlNode configNode, String[] parameters, Boolean assert) +67 Sitecore.Configuration.Factory.CreateObject(XmlNode configNode, String[] parameters, Boolean assert, IFactoryHelper helper) +141 Sitecore.Configuration.Factory.GetProviders(List`1 nodes) +504 Sitecore.Configuration.Factory.GetProviders(String rootPath, TProvider&amp; defaultProvider) +359 Sitecore.Configuration.ProviderHelper`2.ReadProviders() +80 Sitecore.Configuration.ProviderHelper`2.get_Provider() +105 Sitecore.Pipelines.PreprocessRequest.StripLanguage.Process(PreprocessRequestArgs args) +73 (Object , Object[] ) +83 Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args) +365 Sitecore.Nexus.Web.HttpModule.??(Object ??, EventArgs ??) +284 System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +80 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean&amp; completedSynchronously) +165

What could be the issue? I double checked the class names and name spaces. Any pointers should help here.

Alistair Deneys

Hi Vaibhav, If you've double checked the namespace and class names then the last thing to check would be the assembly name, which is the name after the comma (,) in the type name. In your error message above you've specified the assembly name as "CustomBucketUrl", so make sure your code is compiled into an assembly of that name or change the reference in your configuration to use the proper name of the assembly.

Andrew Quaschnick

I am getting the same error listed above. I attached Visual Studio to the application and run debug and its not even getting to this code. I added the classes you described above and added the lines to the configuration file as well. What should I do next?

Alistair Deneys

Hi Andrew, If you're absolutely sure you've compiled and deployed everything correctly the next thing to do is use the /sitecore/admin/ShowConfig.aspx utility to inspect the fully patched configuration. If your config updates don't appear in there then the problem likely lies with your config patch file. Ensure the config patch is deployed in the App_Config\Include folder and that it uses a '.config' extension.

John

I'm receiving an error when trying to install a TDS package when using the update installation wizard. I've determined its directly related to the CustomItemResolver. After hitting the install button, I immediately receive this error within the 'More Information' pane: ******************************************** Server Error in '/' Application.
Object reference not set to an instance of an object.
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
Stack Trace:
[NullReferenceException: Object reference not set to an instance of an object.] Sitecore.Pipelines.HttpRequest.HttpRequestArgs.GetItem(String path) +163
CustomBucketUrl.CustomItemResolver.Process(HttpRequestArgs args) in CustomBucketUrl\CustomItemResolver.cs:24 (Object , Object[] ) +142 Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args) +364 Sitecore.Nexus.Web.HttpModule.??(Object ??, EventArgs ??) +456 System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +79 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean&amp; completedSynchronously) +164
***********************************************
Line 24 in my code corresponds to this line in the code:
var bucketItem = args.GetItem(bucketPath);
As I said, if I remove the config include file from the webroot, the installation processes successfully.

Alistair Deneys

Hi John, Yeah, that's the thing about the httpRequestBegin pipeline. It will process all requests that Sitecore sees including those to the Sitecore clients. It would seem you'll need to add some additional code to ensure requests being handled by the update installation wizard are ignored by this processor. You could probably be a bit more broad with it and exclude anything starting with /sitecore at the beginning of the request URL.

Erik Melkersson

A note on getting the Resolver to work in Sitecore 7.5. I had to change
using (var searchContext = ContentSearchManager.GetIndex(bucketItem as IIndexable).CreateSearchContext())
into
using (var searchContext = ContentSearchManager.GetIndex((SitecoreIndexableItem)bucketItem).CreateSearchContext())
Otherwise I got "Index was not found" all the time on that row.

Erik Melkersson

A warning: This seems to affect a bug in the permission system for page editor.
If I have a user with write permission on a single item in the bucket that editor can't edit that item using the short address but he/she can edit it using the long address.

Erik Melkersson

Update: This seems to be a rest of another bug (in Sitecore 7.5) where the user can't edit the placeholder contents unless he/she has write access on the placeholder item too.

Even i was getting the above error .. But Erik Suggestion solved the issue.

I am also using the partech module for seo optimization..
While i was doing patch:after the Sitecore.Pipelines.HttpRequest.ItemResolver My processor was not working ... partech also has processor for item resolver...
In order to fix the issue i have added the processor in web.config as below
This worked for me..

Hi Alistair,
I have encountered one wired problem.
I was able to access the site for given item using
http://www.mysite.com/myitem
But at the same time when I entered the special character along with item name and it was working all fine for that given urls as shown below.
http://www.mysite.com/myitem!
http://www.mysite.com/myitem~!
http://www.mysite.com/~myitem~
http://www.mysite.com/~~myitem
http://www.mysite.com/myitem!~
http://www.mysite.com/myitem~~

While investigating we found that the Search(Solr in our case) result was returning the item in all the above case.
We mitigated the problem by replacing the search code by fast query..
It's worked all fine for us.
Please let me know your thought for this.
Many Thanks.

Nick

One thing that I've done to generate my own url is instead of the custom item resolver, I actually create a wild card item (*) in the content tree. Then in my Controller, I take whatever value is passed in for that wild card and search my bucket for the item. I like your approach though.

[&#8230;] there article on how command urls buckets: Item Buckets and&nbsp;URLs [&#8230;]

Mike

Hi Raj, I was getting a similar issue, where the request was returning /sitecore/service/notfound.aspx as the URL instead of my original URL. I then ran /sitecore/admin/showconfig.aspx per the suggestion above and peculiarly, my custom processor was NOT showing up where I expected. Instead of being listed immediately after

it's showing up about 16 lines down.
Turns out, the issue was a missing space! The patch statement copied from the above: &lt;processor patch:after=&quot; processor[@type=&#039;Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel&#039;]&quot;
NEEDS to have a space between &quot;ItemResolver,&quot; and &quot;Sitecore.Kernal&quot;. I was missing that space, and it was injecting it too late in the cycle (because it wasn&#039;t matching the patch on the attribute).
Mike

Shilpa

Hi, I followed evrythung step by step, but I'm getting an error:
Could not resolve type name: CustomBucketUrl.CustomLinkManager, CustomBucketUrl (method: Sitecore.Configuration.Factory.CreateType(XmlNode configNode, String[] parameters, Boolean assert)).
I double checked my namespace and config files. Everything is same as yours but still not able to figure it out.

Alistair Deneys

Did you compile your customization with an appropriate .net version for the Sitecore version you're using? Did you ensure your assembly is deployed to the bin folder of your Sitecore instance?

Hi Alistair,
Nice blog post! This really gives a nice step by step guide to the problem that I was trying to solve!
However, I ran into an issue when using this on Sitecore 8 update 3.
I was getting a null reference exception on:
`// locate item in bucket by name using (var searchContext = ContentSearchManager.GetIndex(bucketItem as IIndexable) .CreateSearchContext())`
The problem was that it was complaining about as suspicious cast for `(bucketItem as IIndexable)`. I was able to resolve that by first creating a SitecoreIndexableItem and then passing that into the `GetIndex()` method. Substitution code is as follows:
`SitecoreIndexableItem indexableItem = new SitecoreIndexableItem(bucketItem);
// locate item in bucket by name using (var searchContext = ContentSearchManager.GetIndex(indexableItem) .CreateSearchContext())`
I thought I would put up this comment so that if anyone else following your post gets the same error, hopefully it will help them.

Thanks, Akshay

Alistair Deneys

Thanks for the heads-up Akshay

Leave a comment

All fields are required.