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