Displaying shared content across multiple Sites

Sitecore does a great job of separating content from the presentation of the content. This is the main selling point of any CMS tool, but Sitecore does an excellent job of allowing this to happen.

What I found not intuitive in Sitecore is the ability to display shared content.

PROBLEM
For instance, assume you have stories which live outside of the context of any site site. The stories can be tagged with specific categories, where each category associates to a different web site. (ok, this is a simple example, but follow me here).

So you have the following structure in your sitecore tree

/sitecore/content/site 1
.. [all of the nodes that make up the site]

/sitecore/content/site 2
..[all of the nodes that make up the site]

/sitecore/content/Stories
2010
January
Story Item 1 [category = site 1]
Story Item 2 [category = site 1 and site 2]
February
Story Item 1 [category = site 1 and site 2]
...etc [you get the picture]

POSSIBLE SOLUTIONS
What are some effective ways to display your story items under each site?
Assume you want a page that displays a listing of all stories under each site. You could create a sublayout which iterates over all stories and adds them to the list if they are categorized for the specific site the sublayout is on. So you would have a list of the stories and could easily add paging if desired.

But what happens when you want to view the details of a story? You could simply create another sublayout and drop it on the standard values of a template, passing the ID of the story to this new sublayout. Then the details of your story could be displayed.

But this solution brings up a problem.... The url of the story is not easily addressable. Passing a guid parameter to a web page is not very clean. What if others want to link directly to a specific story? In summary, the url does not appear SEO friendly nor is it user friendly. I'd rather have the ability for the article to be accessed http://site1/stories/story article1.aspx.

You could look at proxies but proxies would still need to be displayed somehow and when you modify a proxy you are modifying the original item. So you could never have a proxy with different presentation components.

Also, with proxies the structure of the proxied item carries from the original item. What if you don't like the structure of the original item? And according to Sitecore's own documentation proxies do slow down your site (which only makes sense as you add another layer).

Also, what if you want each story to be displayed differently?

PROPOSED SOLUTION
I searched SDN and did not find a solution. So i came up with the following work around. It does not feel like too much of a hack :-)

1. I created a new template based off of the standard template called Base Item Pointer. I added the section Item Pointer and the Droptree field Item Pointed To.

2. I created a a couple new templates (Story Pointer with Sidebar, Story Pointer No Sidebar) using the Base Item Pointer template as the base template. On these new templates I added standard values and setup my presentation components appropriately. NOTE: The two templates I just mentioned were created for site 1. Site 2, Site 3, etc would need their own set of Story Pointer templates.

3. I then created an event handler for the item:added event. My handler simply checks to see if the item added is a story and it creates a new item (based off of either the Story Pointer with Sidebar or Story Pointer with No Sidebar template created in Step #2) underneath site 1's story section. The name of the new item comes from the name of the story article. This allows the Story to be URL addressed appropriately.

4. The presentation components on the Story Pointer with Sidebar and Story Pointer No Sidebar can present the story correctly for the web site they were created for.
Here is some sample code. Right now I am always creating the pointer items in the item:added event. What I will probably do is create and remove the pointer items on the item:saved event so I can verify the data associated with the new story before creating / deleting the pointer items. But the sample code hopefully will give a good idea of what is happening.
To enable an item:added or item:saved event handler you need to modify the web.config. Look for the item:added, item:saved, etc events.


/// <summary>
/// This code checks to see if the user created a story item.
/// 
/// If a story item was created it then creates a new pointer item that points to the story item created.
/// 
/// The new pointer item has the appropriate presentation components.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void OnItemAdded(object sender, EventArgs args)
{
if (args == null)
{
return;
}

//
//Our Web Site Settings template lives in the master database, not in the core.  But the core database
// has the current context as the user is in Sitecore.
//

//
// The CacheHelper class is something we have written internally, not in sitecore
//
Item itemWebSiteSettings = CacheHelper.GetItem(Common.Constants.CFCA_WebSiteSettingsID,"master");

//
// Here we get the item being added using the Sitecore API
//
Item itemAdded = Sitecore.Events.Event.ExtractParameter(args, 0) as Item;

//
// Our web site settings template has the field 'Story Summary Item' which points to the 
// parent location where all story pointer should be created.
//
// For multiple web sites I would have a similiar field on their web site settings template.
// Right now I am only create pointer items to one web site.
if (String.IsNullOrEmpty(itemWebSiteSettings["Story Summary Item"]))
{
throw new Exception("The Story Summary Item must be set for the CFCA \\Web Site Settings Node");
}

//
// Here we are checking the itemAdded and only doing something if the item added was a Story
//
if (itemAdded.TemplateID.Equals(this.idtemplateStoryWithSidebar) ||
itemAdded.TemplateID.Equals(this.idtemplateStoryNoSidebar))
{
Sitecore.Data.Database masterDB = Sitecore.Configuration.Factory.GetDatabase("master");

//
// This is the parent item that contains the pointer items which point to stories
//
Item storySummaryItem = masterDB.GetItem(itemWebSiteSettings.Fields["Story Summary Item"].Value);
if (storySummaryItem == null)
{
throw new Exception("Unable to find the Story Summary Item for the web site.  Check the web site settings.");
}

//
// Here we are creating the appropriate pointer template based on the story template.
//  Our pointer template is what contains the presentation details.
//
Sitecore.Data.Items.TemplateItem linkTemplate = masterDB.GetItem(this.idtemplateStoryPointerWithSidebar);
if (itemAdded.TemplateID.Equals(this.idtemplateStoryNoSidebar))
{
linkTemplate = masterDB.GetItem(this.idtemplateStoryPointerNoSidebar);
}

using (new Sitecore.SecurityModel.SecurityDisabler())
{
//
// Here we create the item which points to the newly added story
//
Item pProxyItem = storySummaryItem.Add(itemAdded.Name, linkTemplate);
pProxyItem.Editing.BeginEdit();

//
// Now we point to the story item.
//
pProxyItem.Fields["Item Pointed To"].Value = itemAdded.ID.ToString();
pProxyItem.Editing.EndEdit();
}
}
}


Update May 6, 2010:  I found out that the item:deleted event is fired after the Sitecore UI dialogs have been presented to the user.  In our case, if a user tries to delete a story they will be prompted with the Broken Links dialog because our pointer item points to the story being deleted.   To alleviate this problem I added to the uiDeleteItems pipeline in the web.config file my own pipeline.  The pipeline simply deletes the pointer item before the BrokenLinks dialog has had a chance to check for referring links.

public class DeleteItems : Sitecore.Shell.Framework.Pipelines.ItemOperation
    {
        /// <summary>
        /// These are the templates the user uses when creating stories.
        /// </summary>
        private ID idtemplateStoryNoSidebar = new ID("{FD026C2A-11F2-4B98-9506-700CC2A03E9A}");
        private ID idtemplateStoryWithSidebar = new ID("{7B5AE3F9-BEBB-43F1-A2E9-05B1316F64EE}");

        private Sitecore.Data.ID baseItemPointTemplateID = new Sitecore.Data.ID("{3D9B7A98-05DE-43F5-B3D8-9AC3815D7708}");

        /// <summary>
        /// These are the templates that have presentation components associated with them.
        /// </summary>
        private ID idtemplateStoryPointerNoSidebar = new ID("{86F915D5-D4AC-45C7-BFAE-3F1F7FF586C7}");
        private ID idtemplateStoryPointerWithSidebar = new ID("{A217B411-F8C4-4A3E-8108-79195C2030F4}");

        public void RemovePointers(ClientPipelineArgs args)
        {
            ListString slistIDs = new ListString(args.Parameters["items"], '|');
            LinkDatabase linkDatabase = Globals.LinkDatabase;
            Database db = GetDatabase(args);

            foreach (string sID in slistIDs)
            {
                Item itemDeleted = db.GetItem(sID);

                if (itemDeleted != null & 
                    itemDeleted.TemplateID.Equals(this.idtemplateStoryNoSidebar) ||
                    itemDeleted.TemplateID.Equals(this.idtemplateStoryWithSidebar))
                {
                    ItemLink[] itemLinks = Sitecore.Globals.LinkDatabase.GetReferrers(itemDeleted);

                    foreach (ItemLink itemLink in itemLinks)
                    {
                        Item itemSource = itemLink.GetSourceItem();

                        if (itemSource != null && 
                            (itemSource.TemplateID.Equals(this.idtemplateStoryPointerWithSidebar) ||
                            itemSource.TemplateID.Equals(this.idtemplateStoryPointerNoSidebar)))
                        {
                            itemSource.Delete();
                        }
                    }
                }
            }
        }

        private static Database GetDatabase(ClientPipelineArgs args)
        {
            Database database = Factory.GetDatabase(args.Parameters["database"]);
            return database;
        }

    }


Here is the web.config file. I put my new pipeline at the beginning so it can happen before sitecore's dialogs.

<uiDeleteItems>
        <processor mode="on" type="CFCAWeb.SC.Pipelines.DeleteItems, CFCAWeb.SC" method="RemovePointers"/>
        <processor mode="on" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel" method="CheckPermissions"/>
    <processor mode="on" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel" method="Confirm"/>
    <processor mode="on" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel" method="CheckTemplateLinks"/>
    <processor mode="on" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel" method="CheckLinks"/>
    <processor mode="on" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel" method="CheckLanguage"/>
    <processor mode="on" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel" method="Execute"/>
   </uiDeleteItems>



If you have other thoughts or ideas let me know.

Thanks -

Rob

Comments

  1. i'm not 100% getting that: when you create an item under the shared tree the event handler is then 'duplicating' the item based on the categories selected and uses the appropriate templates?
    what if you need to edit the text? you then have multiple items... or is the handler overwriting items in the target location?
    or am i getting something wrong here....

    we thought of something similar, but wanted to add a handler to the publishing queue: meaning that we create items under a shared location (outside the sites) and tell the items (or better a root item for a tree) to which site and target item they should publish to. in the publishing queue we then would publish the items to the right targets and overwrite existing ones.

    we have never done that, but it sounds similar to your solution... could you publish the code for that?

    thank you for sharing your ideas!

    ReplyDelete
  2. Hi Reinold -

    The event handler does create a new item in the desired location. But the new item it creates contains one field 'Item Pointed To' which is a Droptree field. The Droptree field then points to the story which was created that fired the item:created event.

    I am also adding event handlers for the item:saved and item:deleted and item:renamed. So that my pointer item can be updated appropriately.

    On the item:saved I can check if the item being saved is a story, if the item is a story I then verify the categories assigned to the story and delete the pointer item if necessary.

    On the item:deleted (if the item is a story) I can simply delete my pointer item.

    On the item:renamed (if the item is a story) I can rename the pointer item so its name matches the name of the story.

    I will post the code.

    ReplyDelete
  3. thank you rob!

    that makes it much clearer for me now! very good idea actually....

    so what are you doing on the sublayout of your pointer items? you are getting the 'item pointed to' field, get the story item and use the fields on this one to populate your content on the pointer item... am i thinking in the right direction here?
    what if you need to index the content under your 'story summary item'? (we need to do that using lucene - and that works fine with our virtual provider...)

    sorry for so many questions... but that smells like a very good solution :-) ours (with virtual providers and stuff...) is far to complicated to be used more often, but yours seems to be quite easy...

    ReplyDelete
  4. Hi Reinold -

    Regarding your question, 'so what are you doing on the sublayout of your pointer items? you are getting the 'item pointed to' field, get the story item and use the fields on this one to populate your content on the pointer item... am i thinking in the right direction here?'

    That is what I am doing. I get the item pointed to and use that to populate the content.

    We have not gotten to the point where we need to index the content under our story item. But here is what I am thinking for a solution; We will index all of the stories (the real stories) and when we go to render the stories in the search results we will get the referrers of the story from the Link database and build the URL using the Pointer item.

    So the user will click the URL we render in the search results and it will take them to the pointer item. The pointer item will then render the story using the layout.

    I figured out how to delete the pointer item when the original item is deleted. Although the users rarely delete items I did want to fix this issue. Instead of using the item:deleted event handler I hooked into the uiDeleteItems pipeline.

    I will explain the deletion of the pointer item and post the code.

    No problem on the questions. I enjoy the discussion.

    Have a great day!

    ReplyDelete

Post a Comment

Popular posts from this blog

Why I chose Selenium WebDriver instead of Telerik's free testing framework

Handling the situation where scItemPath does not exist in Sitecore MVC