Sharing item bucket pages between websites in a multi-site Sitecore 7 instance

Just about every site that I have worked on recently takes advantage of Sitecore's multi-site capabilities to host more that one website within the same Sitecore instance.  Apart from a few extra steps that you need to take to ensure that support teams can keep a reasonable degree of separation between the sites when performing maintenance and release activities (a subject that I mean to come back to in another post), this approach works really well.  One of the key advantages of this approach is the ability to share content between multiple sites.

The challenge

One of the great "selling points" of Sitecore 7 is the ability to keep a large amount of content in item buckets and then interact with it using search.   This also provides a great opportunity for content syndication between Sitecore sites, as the shared content can be centrally managed, but then "included" in separate sites.  However when want to display this content resulting from a search in a page layout specific to each site (and potentially with different placeholders and modules on each site) things get a little more involved, as you can't simply redirect to the URL of the item in the bucket itself.

In this example, we have a central item bucket repository of Car Review content items which we want to be able to filter and display on two different sites in our multi-site instance.

Content tree showing Sitecore 7 item bucket

Within this item bucket we have the review of a 2006 Honda Accord which we want to share between sites.
Car Review item in Sitecore 7 item bucket search

Setting up wildcard items

What we really want to do in this case is to have a separate "detail" page for displaying the content on each site, and have that page find and display the content from the item bucket.  We could be crass about it and pass the ID of the car review item as as a querystring variable, but apart from looking messy we are losing a great SEO opportunity!

Fortunately Sitecore has a much more elegant way of approaching this problem, and that is through the use of Wildcard items.  You can create a wildcard item by creating an item with the name "*" at a point in the content tree.  Sitecore will then serve this item for requests matching any value at that point in the URL path..

In this case, we create a wildcard entry at the path /cars/reviews/*

Content tree showing wildcard item


Displaying item bucket content based on a wildcard

The responsibility of finding and displaying the content falls to the presentation components on the wildcard item.  The wildcard item we created is an instance of a general content page, which has a placeholder to display content in the central content column.  To find and display the individual car review details we create a new sublayout and place it on the wildcard item page.

The following code will look up the item to display based on the item name being passed as the wildcard value.    For example a call to http://MyCarReviews.com/Cars/Reviews/2006-Honda-Accord-VTi will look for a car review  for an item named "2006 Honda Accord VTi" within the Car Review item bucket.  (We would also need to create separate presentation code that will handle searching and filtering of car reviews from the bucket, which will then build links to the review detail pages in the expected format.  I haven't covered that in this post)

        private Item _wildCardItem;
        protected Item WildCardItem
        {
            get
            {
                if (_wildCardItem == null)
                {
                    var raw = WebUtil.GetRawUrl();
                    
                    // get the item name token from the url
                    var idx = raw.LastIndexOf('/');
                    var targetItemName = raw.Substring(idx + 1).Replace('-', ' '); // convert back into the original item name string


                    var carReviewBucket = new SitecoreIndexableItem(
                                                    Sitecore.Context.Database.GetItem(
                                                        new Sitecore.Data.ID(
                                                              Guid.Parse("{3AA38259-4E2B-4EF3-A7B3-E91503B4723C}"))));

                    using (var context = ContentSearchManager.GetIndex(carReviewBucket).CreateSearchContext())
                    {
                        var matchingItems = context.GetQueryable<SearchResultItem>().Where(i => i.Name == targetItemName).ToList();
                        _wildCardItem = Sitecore.Context.Database.GetItem(matchingItems.First<SearchResultItem>().ItemId);
                    }
                }
                return _wildCardItem;
            }
            set
            {
                _wildCardItem = value;
            }
        }

A couple of things to note in the above code snippet:

  • When we look for matching items, we are creating a search context getting the index that contains the car review bucket.

using (var context = ContentSearchManager.GetIndex(carReviewBucket).CreateSearchContext())

  • Before we can use the carReviewBucket item as a search index, we must first convert it into a class that implements the IIndexable interface.  For Sitecore items this means wrapping in a SitecoreIndexableItem.

var carReviewBucket = new SitecoreIndexableItem(Sitecore.Context.Database.GetItem(new Sitecore.Data.ID(Guid.Parse("{3AA38259-4E2B-4EF3-A7B3-E91503B4723C}"))));

  • We could further refine this to only match car reviews by restricting by the template ID of the template.

Switching the Context Item

Now we want to place some field renderers on our sublayout to output the values of the car review content retrieved from the Item Bucket.


<%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Kernel" %>


<sc:FieldRenderer runat="server" id="frTitle" FieldName="Title" EnclosingTag="h1" DisableWebEditing="false" />
<sc:FieldRenderer runat="server" id="frImage" FieldName="Image" DisableWebEditing="false" />
<sc:FieldRenderer runat="server" id="Summary" FieldName="Summary" DisableWebEditing="false" />

However for this to work as expected the car review retrieved from the item bucket will need to be the context item.  Fortunately Sitecore has a really simple approach for making sure that we can do this -- the ContextItemSwitcher.

protected override void Render(HtmlTextWriter writer)
{
            
    if (this.WildCardItem != null)
    {
        using (new Sitecore.Data.Items.ContextItemSwitcher(this.WildCardItem))
        {
            base.Render(writer);
        }
    }
    else
    {
        base.Render(writer);
    }
}

Any code that is run inside the "using" block defining the ContextItemSwitcher will treat the item passed to it as the context item.  In this case we pass the WildCardItem which is set to the Car Review that we looked up earlier.  When this context item is set like this Sitecore functionality works pretty much as expected, this includes Page Editor support for the FieldRenderers that we defined.  As a bonus, using the ContextItemSwitcher also allows us to specify Vary By Data caching on the sublayout - which will then cache output HTML varying by the target wildcard item.

Here is the final result.  We can then repeat the process of creating a wildcard item at the appropriate location in other site trees (based on templates/layouts from those sites) and then either share this sublayout that we have created, or create a similar one to layout the content differently as required.
Screenshot of final output


You can find more details on using Wildcard items in the Sitecore Reusing and Sharing Data documentation.

Thanks to Dan McGuane at Sitecore Australia for pointing me in the direction of the ContextItemSwitcher code, and all the benefits that it confers including HTML Output Caching support.

Comments

  1. PLease share the full source code .. as I need to implement the same in MVC

    ReplyDelete
  2. Thanks cam. Hey Abhay - you are there in all the place. You still need code I can request to Cam. We are working in the same team:-)

    ReplyDelete

Post a comment