Level: 300
 

Implement single source publishing in Sitecore

Page embedding

This article presents an approach to implement single source publishing in Sitecore. The article includes a discussion of the implications that single source publishing has on website search.

Written by: Lennert Sloth
Wed, Sep 23 2009

 

 

 

Sometimes you want to share content (The content in question here is not just small pieces of graphics or text shared across a website, but significant parts of a page) between different pages in a website. For instance, you might write an article describing a specific reference your company has. It might be relevant to show this article in two or more different sections of your website.

 

What you would like to achieve is to store the article as a single entity but be able to display the article in multiple different contexts (pages) so that when you make changes to the article you only have to do it in one place and the changes will automatically be reflected in all the pages that “include” the article. One possible approach to implement this in Sitecore CMS is presented here.

 

 

The structure of the website

The content for the website is divided into two branches under the “/sitecore/content” node,


 

  • /sitecore/content/SharedContent/
  • /sitecore/content/Home/

 

 

Shared Content

The "/sitecore/content/SharedContent" sub tree contains the items that contain content that we want to show in multiple web pages. For instance, we have the following shared content item, /sitecore/content/SharedContent/References/Reference1.

 

 

 

Web pages 

All the web pages for the site are stored in the "/sitecore/content/Home" sub tree. For instance we have the these two items,

 

  • /sitecore/content/Home/Marketing/Page1
  • /sitecore/content/Home/News/Page2


Both Page1 and Page2 should show the content stored in Reference1 as illustrated in Figure 1.

 

 

Page embedding

Figure 1.Two page content items "embedding" the same shared content item

 

 

The Reference1 item does not represent a web page in its own right and hence it has no layout. Page1 and Page2 on the other hand represent web pages and therefore they do have a layout. These layouts includes a rendering that renders the content of Reference1 making it appear as though it is embedded inside Page1 and Page2.

 

 

Templates

The template for Page1 and Page2 has a Droptree field that makes it possible to include Reference1. (Page1 and page2 are not necessarily based on the same template but for this example it is assumed that their template(s) has a field called Reference of type Droptree.  )

 

 

Reference page template

Figure 2 show parts of the template for the pages.

 

 

 

Presentation

As mentioned above the layout for Page1 and Page2 contains a control that renders the content of Reference1 as an integrated part of the pages. The xsl below shows how to get hold of the Reference item enabling us to render its content.

 

 

<!-- Snippet from Reference.xslt which is used to present our pages. -->

<!-- Get the ID of the reference item -->

< xsl:variable name = "referenceId" 

  select = "sc:fld('Reference',$referenceItem)"/>

 

 

 

 

 

<!-- Get the reference item from the ID -->

< xsl:variable name = "referenceItem"

  select = "sc:item($referenceId,$sc_currentitem)" />

 

 

The item variable $referenceItem now represents the Reference item and assuming it has a field named text that contains the content we want to show, we can render the content like this,

 

 

<xsl:if test="(sc:fld('text',$referenceItem)!=''">

  <sc:text field="text" select="$referenceItem"/>

</xsl:if>

 

 

It is not a requirement that the shared content items are based on the same template. All you need to know is the name and type of the fields that should be shared. To ease the maintainability of the site, it is probably a good idea to implement some rules guarding which names to use for the shared fields.

 

 

Single source publishing and website search

One thing to keep in mind when implementing this kind of single source publishing for a website is that, if you do not do something specific to prevent it, a search for a phrase that is present in the shared content will result in multiple hits for the same content since multiple pages have the same content embedded. Whether this is acceptable/desirable may depend upon the kind of website you are building.

 

 

Lucene Search Engine and searching in shared content

The approach to single source publishing described above does however introduce a problem if you are using the Lucene Search Engine or similar to implement search for your website. If you perform a search using Lucene “out of the box” on a site structured as outlined above, some of the hits returned by Lucene will be shared content items located in the “/sitecore/content/SharedContent/” branch. These items do not represent complete web pages and have no layout so you cannot present them directly as search result to the user. What you want to present to the user is the page items (Page1 and Page2 in the example above) that embed the shared content items.

 

 

One way to solve this problem is to define your own index and write your own indexer. There is a great article describing how to do that here.

 

 

When defining the index in web.config you can specify which templates the items must be based on in order to be included in the index. We use this to make sure that only items in the “/sitecore/content/Home/” branch of the content tree are directly included in the index.

 

 

Now, whenever a page item, X, in the “/sitecore/content/Home/” (e.g. Page1 in the example above) branch links to a shared content item, Y, in the “/sitecore/content/SharedContent/” (e.g. Reference1 in the example above) branch, we index the Y item as though it is a part of item X. This is implemented in the AddFields method in the code listing below. Doing this makes the shared content items appear in the search results as though they are “normal” parts of the page items. This idea was given to us by Jens Mikkelsen from http://learnsitecore.cmsuniverse.net.

 

 

The custom indexer that we have implemented for our web search is listed below.

 

 

public class CustomWebSearchIndexer : Sitecore.Data.Indexing.Index

{

  //Call the base template 

  public CustomWebSearchIndexer(string name) : base(name) { }

 

  protected override void AddFields(Sitecore.Data.Items.Item item,

                                    Lucene.Net.Documents.Document document)

  {

   if (item.Paths.IsContentItem)

   {

     //Call the base to add all fields normally 

     base.AddFields(item, document);

     

     Sitecore.Data.Fields.ReferenceField referenceField = item.Fields["reference"];

     if (referenceField == null)

     { //TODO: handle case that field does not exist 

     }

     else if (referenceField.TargetItem == null)

     { //TODO: handle case that user has not selected an item 

     }

     else

     {

       try

       {

        //Get the Reference item

        Sitecore.Data.Items.Item referenceItem = referenceField.TargetItem;

 

        //Get the text field

        Sitecore.Data.Fields.Field textField = referenceItem.Fields["text"];

 

        //Define the Lucene field

        Sitecore.Xml.Xsl.XslHelper xslHelper = new Sitecore.Xml.Xsl.XslHelper();

        string htmlContent = xslHelper.striptags(textField.Value);

        Field referenceField = new Field("_content", htmlContent, Field.Store.NO, Field.Index.TOKENIZED);

 

         //Add the reference field to the document 

         document.Add(referenceField);

 

         //Get the Title field

         Sitecore.Data.Fields.Field titleField = referenceItem.Fields["title"];

 

         //Define the Lucene field

         string titleContent = titleField.Value;

         Field referenceTitleField = new Field("_content", titleContent, Field.Store.YES, 

                                                Field.Index.TOKENIZED);

 

         //Add the title field to the document 

         document.Add(referenceTitleField);

       }

       catch(Exception ex)

       {

         Log.Error("Error adding 'reference' document to index. Index: " + 

                    Name, ex, this);

        }

      }

 

    }

  }

 

  public override void UpdateItem(Item item) 

  {

    if (item.Paths.IsContentItem)

    {

      base.UpdateItem(item);

    }

  }

  

  protected override void UpdateItem(Item item, bool removeOld, bool updateAllVersions, Lucene.Net.Index.IndexWriter writer)

  {

    if (item.Paths.IsContentItem)

    {

      base.UpdateItem(item, removeOld, updateAllVersions, writer);

    }

  }

  

  public override string GetDocID(ID itemId)

  {

            return ShortID.Encode(itemId);   }

 

  public override void RemoveItem(ID itemId, Database database)

  {

            Assert.ArgumentNotNull(itemId, "itemId");

            Assert.ArgumentNotNull(database, "database");

 

            string docId = GetDocID(itemId);

 

            RemoveItem(docId, database);

  }

  

  void RemoveItem(string docId, Database database)

  {

            lock (SyncRoot)

            {

                using (new LockScope(GetIndexDirectory(database), "write.lock"))

                {

                    IndexReader reader = OpenReader(database);

 

                    if (reader != null)

                    {

                        try

                        {

                            RemoveItem(docId, database, reader);

                            reader.Close();

                        }

                        catch (Exception ex)

                        {

                            Log.Error("Error removing document(s) from index. Index: " + Name

+ ", DocId: " + docId, ex, this);

                        }

                    }

                }

              }

  }

 

  void RemoveItem(string docId, Database database, IndexReader reader)

  {

    Hits hits = new IndexSearcher(reader).Search(new PrefixQuery(new Term(DocIDFieldName,

docId)));

     for (int i = 0; i < hits.Length(); ++i)

     {

       reader.DeleteDocument(hits.Id(i));

     }

  }

}

 

The UpdateItem, GetDocID and RemoveItem methods was provided to us by Sitecore support. Without them the index is not updated automatically when an item is deleted (Sitecore CMS version 6.01).

 

Please rate this article


1 rates / 4 avg.

  • About the author:

    Lennert Sloth

    Lennert Sloth is currently employed by Mjølner Informatics where he is working as a senior software developer. For a number of years he has mainly been involved in web projects, ASP.NET as well as Java based solutions. He has a M. Sc. in Computer Science with a bachelor in physics from University of Aarhus.

4 responses to "Implement single source publishing in Sitecore"

Great article.

Initially I thought, "ok, - fine that somebody is covering the stuff you see all the time!", but then you raised the challenge with Lucene search, which is an often forgotten issue.

Off course, using 3rd part search engine (e.g. ForwardSearch or Coveo), this will not pose a problem.

Idea: An alternative way of solving this challenge is by using proxy items which inherits their layout as described in Alistair Deneys blog http://adeneys.wordpress.com/2009/08/11/composite-presentation-inheritance/.

I like your article approach about describing the base problem, then extend it with the challenges such a solution causes (in this case, search was the problem).

Good job
Posted: Thursday, September 24, 2009 7:53 AM
Lars, my experience with proxies is that they should only be used in very very small scale... If over used then they will impact performance on both back and frontend. At least in Sc53 ;-)
Posted: Thursday, September 24, 2009 7:27 PM
vbn
dffgfg
Posted: Tuesday, July 22, 2014 1:28 PM
I'm having an issue with geinttg the onFling to work as described in this example. I've discovered, that in order for the more advanced gestures to work, one needs to consume the onDown event.In this case it means that one needs to override the onDown in the custom MyGestureDetector and always return true.Altogether the solution is lovely, thank you very much. I had troubles making the default Gallery widget work with downloadable content and here I can limit the amount amount amount amount amount amount amount user can move until the content gets downloaded.
Posted: Friday, December 25, 2015 8:01 PM

Leave a reply


Notify me of follow-up comments via email.
 
 
#nbsp;