Item Buckets and URLs

24

July 19, 2013 by Alistair Deneys

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.

Advertisements

24 thoughts on “Item Buckets and URLs

  1. bsvachzi says:

    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/

  2. 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 says:

      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.

  3. […] If you give the Sitecore 7.0 build a go one thing you may be questioning is why we left the News Mover module in there and didn’t change to using Item Buckets. The biggest issue is the fact that you can’t use a field on the item to determine the item bucket folder structure. And because we want to base the URL on the post date (which is now a new field) and not the created date, we can’t achieve that with Item Buckets. On a side note, the more I use Item Buckets the more I realise that you shouldn’t worry about the folder structure of the bucket or expose that folder structure to the world. See my previous post on Item Buckets and URLs. […]

  4. Raj says:

    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 says:

      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.

      • Mike says:

        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:
        <processor patch:after="
        processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"

        NEEDS to have a space between "ItemResolver," and "Sitecore.Kernal". I was missing that space, and it was injecting it too late in the cycle (because it wasn't matching the patch on the attribute).

        Mike

  5. Vaibhav says:

    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& 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& completedSynchronously) +165

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

    • Alistair Deneys says:

      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 says:

        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 says:

        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.

  6. John says:

    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& 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 says:

      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.

  7. […] year I wrote a post on customising the URLs used to access items within an Item Bucket when using Sitecore 7+. The basic idea was that the structure of the bucket was completely ignored. […]

  8. Erik Melkersson says:

    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.

  9. Erik Melkersson says:

    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 says:

      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.

  10. 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..

  11. 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.

  12. Nick says:

    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.

  13. […] there article on how command urls buckets: Item Buckets and URLs […]

  14. Shilpa says:

    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 says:

      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?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories

The views expressed on this blog are solely my own and do not necessarily reflect the views of my employer.
%d bloggers like this: