Reinoud van Dalen

September 23, 2015

Sitecore multisite placeholder settings

This issue has been on the outskirts of my radar for a while, Sitecore’s placeholder settings strategy is pretty straight forward and not optimal for multisite environments. While working on a generic presentation framework I felt it was time to address this issue.

The solution in short

undefinedI ended up with a processor which needs to be first in the getPlaceholderRenderings pipeline. It will try to resolve a site specific placeholderItem and if it does it will pass it along to the rest of the processors. So if the current item resides on the default website called ‘website’ and the current placeholderKey is ‘column’ then it will resolve ‘website-column’ whereas other sites will use the default column settings.

The source code:
https://github.com/RvanDalen/Sc.Commons.SitePlaceholders

Nuget package:
https://www.nuget.org/packages/Sc.Commons.SitePlaceholders

First approach: use a folder convention

So at first I thought it would be nice to have site specific folders and prefer those placeholder settings over the root ones. But looking at the getPlaceholderRenderings pipeline I concluded that there was no suitable place to hook in and create a fallback mechanism. And since I am a big fan of DynamicPlaceholders this would have been nice, but sadly I was not able to inject these rules into the PageContext.GetPlaceholderItem.

Second approach: try to resolve a specific placeholderKey

Having just hit a wall on the first convention I thought it may be an option to prefix site specific settings. Then if I’m able to resolve it I want to pass it along to the rest of the processors so they would be able to do the rest.

The processor looks like this (added comments to clarify what the sections are responsible for):

[UsedImplicitly]
public class ResolveSiteSpecificPlaceholderKey
{
//copied from Sitecore.Support.Pipelines.GetPlaceholderRenderings.GetDynamicKeyAllowedRenderings
private readonly Regex _regex = new Regex("(.+)_[\\d\\w]{8}\\-([\\d\\w]{4}\\-){3}[\\d\\w]{12}");

[UsedImplicitly]
public void Process(GetPlaceholderRenderingsArgs args)
{
Assert.IsNotNull(args, "args");
var placeholderKey = args.PlaceholderKey ?? string.Empty;

//filter out the guid if its a dynamically generated placeholder
var match = _regex.Match(placeholderKey);
if (match.Success && match.Groups.Count > 0)
placeholderKey = match.Groups[1].Value;

//get current item's sitename and compile a site specific placeholderKey
var siteName = GetSiteName();
if (string.IsNullOrEmpty(siteName)) return;

var placeholderKeySegments = placeholderKey.Split('/');
placeholderKeySegments[placeholderKeySegments.Count() - 1] = string.Format("{0}-{1}", siteName, placeholderKeySegments[placeholderKeySegments.Count() - 1]);
var siteSpecificPlaceholderKey = string.Join("/", placeholderKeySegments);

//try to resolve the specific placeholderKey
var page = Client.Page;
Assert.IsNotNull(page, "page");

Item placeholderItem;
if (ID.IsNullOrEmpty(args.DeviceId))
{
placeholderItem = page.GetPlaceholderItem(siteSpecificPlaceholderKey, args.ContentDatabase, args.LayoutDefinition);
}
else
{
using (new DeviceSwitcher(args.DeviceId, args.ContentDatabase))
placeholderItem = page.GetPlaceholderItem(siteSpecificPlaceholderKey, args.ContentDatabase, args.LayoutDefinition);
}

//adjust the placeholderKey if we were able to resolve it
//unfortunately the args.PlaceholderKey has no setter so for now this is fixed with some casual Reflection hacking
if (placeholderItem != null)
{
var placeholderKeyField = typeof (GetPlaceholderRenderingsArgs).GetField("placeholderKey", BindingFlags.Instance | BindingFlags.NonPublic);
if (placeholderKeyField != null) placeholderKeyField?.SetValue(args, siteSpecificPlaceholderKey);
//args.PlaceholderKey = siteSpecificPlaceholderKey;
}
}

private string GetSiteName()
{
//get the id from the querystring
var queryString = HttpContext.Current.Request.QueryString;
var itemId = queryString["id"];
string siteName = null;

if (!string.IsNullOrEmpty(itemId))
{
//get the sitecore item, which is the context item of the page in the xEditor
var pageItem = Client.ContentDatabase.GetItem(itemId);
if (pageItem != null)
{
//match it with a content site
foreach (var info in SiteContextFactory.Sites.Where(info => !string.IsNullOrEmpty(info.RootPath) && (info.RootPath != "/sitecore/content" || info.Name.Equals("website"))))
{
if (pageItem.Paths.FullPath.StartsWith(info.RootPath))
{
siteName = info.Name.ToLowerInvariant();
break;
}
}
}
}

return siteName;
}
}

This actually worked but the GetPlaceholderRenderingsArgs.PlaceholderKey does not have a setter. This was a real bummer. Having everything worked out, from getting the current item’s sitename to respecting dynamically generated placeholderKeys and resolving the placeholder item I was unable to pass it along to the rest of the processors.

I ended up using a bit of Reflection hacking to force set the backing field and voila! Not sure if this is the most elegant solution but it works, whoop whoop.

TAGS: sitecore multisite