[TechSpeak] Removing Orphaned Renderings in Sitecore
Let’s take a break from AI news — today, I want to share a real-world challenge I encountered while working with Sitecore 10: orphaned renderings.
Hire SaM Solutions’ Sitecore team to perform a tailored audit of your Sitecore implementation and highlight points for improvement.
Problem
While working on a current content project in Sitecore 10, I encountered a frustrating issue: when removing a component that contains a placeholder, Sitecore deletes only the parent rendering but leaves all nested child renderings behind in the layout field (__Renderings
). As a result, orphaned components are created. They will never be rendered but still remain in the item’s layout, leading to unnecessary clutter, rendering issues, and potential performance degradation.
When working with Sitecore’s dynamic placeholders, child components are often assigned within parent renderings. However, if a parent rendering is deleted, Sitecore does not automatically clean up the child renderings that were nested inside its placeholder.
These orphaned components:
- Remain stored in the layout field (
__Renderings
). - Are not visible on the page, even though they still exist in the data structure.
- Can cause broken layouts, unused placeholders, and excess rendering data.
Solution
So, how do we clean up these orphaned renderings? Let’s dive into the fix.
As part of the solution, we implemented an event handler for item:saved
to validate whether a rendering with a nested placeholder was removed, potentially leaving orphaned renderings. If orphaned renderings are detected, they are removed, and the layout field is reserialized.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Events;
using Sitecore.Layouts;
namespace YourNamespace.Sitecore.Events
{
public class DeleteOrphanRenderingsHandler
{
private const string DefaultDeviceId = "{FECA918C-40C4-4F99-8CF6-BE64DAF8FC32}"; // Default Sitecore device ID
private const string PlaceholderFormat = "-{0}-"; // Format for placeholders in Sitecore
private const string LocalDataSourcePrefix = "local:"; // Prefix for local data sources
public void OnItemSaved(object sender, EventArgs args)
{
if (!(Event.ExtractParameter(args, 0) is Item savedItem) ||
!(Event.ExtractParameter(args, 1) is ItemChanges itemChanges) ||
itemChanges.IsEmpty)
{
return;
}
foreach (var fieldId in new[] { Sitecore.FieldIDs.LayoutField, Sitecore.FieldIDs.FinalLayoutField })
{
if (!itemChanges.FieldChanges.Contains(fieldId))
continue;
// Important check: If Context.Item is null, local data sources may break (render empty)
if (Context.Item == null)
{
Context.Item = savedItem;
}
var updatedField = itemChanges.FieldChanges[fieldId];
var updatedFieldValue = updatedField.Value;
var originalFieldValue = updatedField.OriginalValue;
if (string.IsNullOrEmpty(originalFieldValue) || string.IsNullOrEmpty(updatedFieldValue))
continue;
var originalLayout = LayoutDefinition.Parse(originalFieldValue);
var updatedLayout = LayoutDefinition.Parse(updatedFieldValue);
var deviceId = Context.Device?.ID.ToString() ?? DefaultDeviceId;
var originalDevice = originalLayout.GetDevice(deviceId);
var updatedDevice = updatedLayout.Devices.Cast<DeviceDefinition>()
.FirstOrDefault(x => x.ID == deviceId);
if (updatedDevice == null || originalDevice == null)
{
continue;
}
var updateLayoutField = (LayoutField)savedItem.Fields[fieldId];
var originalRenderings = originalDevice.Renderings.Cast<RenderingDefinition>().ToArray();
var updatedRenderingsFromLayout = updateLayoutField.GetReferences(Context.Device).ToList();
var removeResult = RemoveOrphanedRenderings(originalRenderings, updatedRenderingsFromLayout);
if (!removeResult.wasUpdated) continue;
var mappedRenderings = removeResult.updatedRenderings.Select(x =>
{
var dataSourcePath = x.Settings.DataSource;
return new RenderingDefinition
{
UniqueId = x.UniqueId,
Placeholder = x.Placeholder,
ItemID = x.RenderingID.ToString(),
Parameters = x.Settings.Parameters,
Datasource = ConvertToLocalDataSource(savedItem.Paths.FullPath, dataSourcePath)
};
}).ToList();
updatedDevice.Renderings = new ArrayList(mappedRenderings);
savedItem.Editing.BeginEdit();
// If we use LayoutField.SetFieldValue(), the layout will be missing for the item.
savedItem.Fields[fieldId].Value = updatedLayout.ToXml();
// Prevent second item:save event
savedItem.Editing.EndEdit(true);
}
}
private static (bool wasUpdated, List<RenderingReference> updatedRenderings) RemoveOrphanedRenderings(
RenderingDefinition[] originalRenderings,
List<RenderingReference> updatedRenderings
)
{
var wasUpdated = false;
var removedRenderings = originalRenderings
.Where(orig => updatedRenderings.All(updated => updated.UniqueId != orig.UniqueId))
.ToList();
if (!removedRenderings.Any()) return (false, updatedRenderings);
foreach (var removedRendering in removedRenderings)
{
var updatedRenderingsTemp = updatedRenderings
.RemoveWhere(x => x.Placeholder.Contains(string.Format(PlaceholderFormat, removedRendering.UniqueId))).ToList();
if (updatedRenderingsTemp.Count != updatedRenderings.Count)
{
wasUpdated = true;
}
updatedRenderings = updatedRenderingsTemp.ToList();
}
return (wasUpdated, updatedRenderings);
}
private static string ConvertToLocalDataSource(string savedItemFullPath, string absoluteDataSourcePath)
{
if (string.IsNullOrEmpty(savedItemFullPath) || string.IsNullOrEmpty(absoluteDataSourcePath))
return absoluteDataSourcePath;
if (absoluteDataSourcePath.StartsWith(savedItemFullPath, StringComparison.OrdinalIgnoreCase))
{
var localPath = absoluteDataSourcePath.Substring(savedItemFullPath.Length).TrimStart('/');
return $"{LocalDataSourcePrefix}{localPath}";
}
return absoluteDataSourcePath;
}
}
}
The configuration file should be placed under the App_Config
folder, following your project’s folder structure. If you are using the Helix architecture, I recommend placing it under App_Config/Include/Foundation
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<events>
<event name="item:saved">
<handler type="YourNamespace.Sitecore.Events.DeleteOrphanRenderingsHandler, YourNamespace.Sitecore"
method="OnItemSaved" resolve="true"/>
</event>
</events>
</sitecore>
</configuration>
Some Interesting Moments from the Code Above
Set Context.Item to the current saved item manually
When handling Sitecore’s item:saved
event, Context.Item
is not automatically set to the saved item, even if resolve="true"
is specified in the configuration file. This leads to issues when resolving local data sources (e.g., local:/Data/RichTextDatasource
) because Sitecore relies on Context.Item
for resolution.
Without explicitly setting Context.Item
, the following problem occurs:
updateLayoutField.GetReferences(Context.Device)
; loads local data sources as empty strings (“”).- All renderings that had local data sources will lose their assigned values.
- This causes unexpected behavior and potential errors on the website.
Fix: manually set Context.Item
To ensure correct resolution, manually set Context.Item
before processing:
if (Context.Item == null)
{
Context.Item = savedItem;
}
Using direct field set instead of LayoutField.SetFieldValue()
Using LayoutField.SetFieldValue()
was found to remove the assigned layout, preventing the page from rendering properly.
savedItem.Editing.BeginEdit();
savedItem.Fields[fieldId].Value = updatedLayout.ToXml();
// Prevent second item:save event
savedItem.Editing.EndEdit(true);
ConvertToLocalDataSource function
Datasource = ConvertToLocalDataSource(savedItem.Paths.FullPath, dataSourcePath)
The ConvertToLocalDataSource
function was introduced to convert absolute data source paths to local relative paths when possible. This helps simplify the maintenance of data source items, especially when the parent item is renamed, ensuring that data sources remain correctly linked.
Purpose of this function
- Converts absolute paths (e.g.,
/sitecore/content/Home/Data/Item
) to local paths (local:Data/Item
) when applicable. - Ensures that data sources remain correctly associated with their parent item.
- Prevents issues where renaming a parent item breaks hardcoded absolute paths.
This approach helps keep data sources flexible and reduces the risk of broken references when items are moved or renamed in Sitecore.
P. S.
Thank you for reading! I’m planning to share more about the challenges I encounter in Sitecore development. Stay tuned for more insights!