Publishing Sitecore media and data source items

Reason

Every so often I hear Sitecore editors state that items are not being “published properly” in their solution. The reason for this statement more often than not turns out to be an editor habitually publishing one page at a time, which means related content such as pictures in the Media Library are not published along with it.

On top of having to remember to publish content referenced via “regular item fields”, editors accustomed to publishing one item at a time now face the challenge posed by data sources, which are becoming more and more prevalent due to their extensive use in the Sitecore Digital Marketing Suite.
The “publish a single item”-approach is unfeasible if not obsolete in these kind of solutions, since a page’s layout in itself often references dozens of items spread throughout the content tree — finding and publishing these items individually varies from slightly inconvenient to practically impossible, depending on content structure and volume.

Proper use of workflows, the Workbox and the “Incremental Publish”-function go a long way in terms of ensuring that “no content is left behind” when publishing. That being said, many companies have editorial staff that haven’t had any formal Sitecore training and hence don’t know how to utilize these features, haven’t been given any guidelines by their Sitecore consultants, or have employees who simply feel that clicking “Publish Item” and hoping for the best is preferable to opening the Workbox and proofing/approving related content.
In short, conveying and enforcing the correct use of available publishing mechanisms is often a challenge for customers and consultants alike.

In some cases issues caused by “single page publishing”  can be mitigated by using a custom publish processor as outlined below.

The following code is an example of a publishing processor which adds data sources and other referenced content to the publishing queue, reducing the chance of related content remaining unpublished (i.e. media items referenced in image and file fields, items selected in a tree list, data sources of sublayouts etc.).

Examples are based on Sitecore 6.6 and .NET 4.5.

Code

Copy the markup shown below into a configuration file (e.g. “Publishing.config”) and place it in the “[webroot]/App_Config/Include” folder. Replace namespace and class name as needed.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <publish>
        <processor type="NamespaceName.AddRelatedItemsToQueue, AssemblyName" patch:after="*[@type='Sitecore.Publishing.Pipelines.Publish.AddItemsToQueue, Sitecore.Kernel']"/>
      </publish>
    </pipelines>
  </sitecore>
</configuration>

The publish processor shown below should be tailored to meet the requirements of the editors, solution architecture and content structure.
E.g., should …

  • references of data sources be published?
  • data sources of data sources be published?
  • descendants of data sources be published when “Publish subitems” is selected (i.e. “PublishOptions.Deep == true”)?
  • the workflow step of referenced items change if they’re not in a publishable state?
  • etc.
using System.Collections.Generic;
using System.Linq;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Layouts;
using Sitecore.Links;
using Sitecore.Publishing;
using Sitecore.Publishing.Pipelines.Publish;

public class AddRelatedItemsToQueue : PublishProcessor
{
  private Database _sourceDatabase;

  public override void Process(PublishContext context)
  {
    Assert.ArgumentNotNull(context, "context");
    Log.Info(GetType().FullName + ": Adding items to publish queue - START", this);
    _sourceDatabase = context.PublishOptions.SourceDatabase;
    IEnumerable<ID> itemIDs = GetRelatedItemIDs(context.PublishOptions);
    IEnumerable<PublishingCandidate> publishingCandidates = itemIDs.Select(itemId => new PublishingCandidate(itemId, context.PublishOptions)).ToArray();
    context.Queue.Add(publishingCandidates);
    Log.Info(GetType().FullName + ": Adding items to publish queue - END", this);
  }

  private IEnumerable<ID> GetRelatedItemIDs(PublishOptions options)
  {
    IEnumerable<ID> publishQueueItems = GetPublishQueueItems(options);
    IEnumerable<ID> publishQueueReferencedItems = publishQueueItems.SelectMany(GetReferences).ToArray();
    // In later versions of Sitecore the "referencedItems" collection
    // will contain data sources as well (Sitecore 6.6 Update-4 if I'm not mistaken).
    IEnumerable<ID> publishQueueDataSourceItems = publishQueueItems.SelectMany(GetDataSources).ToArray();
    HashSet<ID> allIDs = new HashSet<ID>();
    allIDs.UnionWith(publishQueueReferencedItems);
    allIDs.UnionWith(publishQueueDataSourceItems);
    return allIDs;
  }

  private IEnumerable<ID> GetPublishQueueItems(PublishOptions options)
  {
    if (options.Mode == PublishMode.Incremental)
      return PublishQueue.GetPublishQueue(options).Select(candidate => candidate.ItemId).ToArray();
    return PublishQueue.GetContentBranch(options).Select(candidate => candidate.ItemId).ToArray();
  }

  private IEnumerable<ID> GetReferences(ID itemID)
  {
    Item item = _sourceDatabase.GetItem(itemID);
    if (item == null)
      return Enumerable.Empty<ID>();
    ItemLink[] references = Globals.LinkDatabase.GetItemReferences(item, true);
    return references.Select(reference => reference.TargetItemID).ToArray();
  }

  private IEnumerable<ID> GetDataSources(ID itemID)
  {
    Item item = _sourceDatabase.GetItem(itemID);
    if (item == null)
      return Enumerable.Empty<ID>();
    string layoutXml = item[FieldIDs.LayoutField];
    if (string.IsNullOrEmpty(layoutXml))
      return Enumerable.Empty<ID>();
    IEnumerable<DeviceDefinition> devices = LayoutDefinition.Parse(layoutXml).Devices.Cast<DeviceDefinition>();
    IEnumerable<RenderingDefinition> renderings = devices.SelectMany(device => device.Renderings.Cast<RenderingDefinition>());
    IEnumerable<string> pathOrIDs = renderings.Select(rendering => rendering.Datasource).Where(pathOrID => !string.IsNullOrEmpty(pathOrID)).ToArray();
    return pathOrIDs.Select(_sourceDatabase.GetItem).Where(dataSourceItem => dataSourceItem != null).Select(dataSourceItem => dataSourceItem.ID).ToArray();
  }
}

Example

These are not the screenshots you’re looking for. Move along.

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