Umbraco's forgotten child: Segmentation in Umbraco 10+
Dennis
Posted on September 6, 2023
Did you know that Umbraco allows you to vary your content by more than just language? You may have come across the word "segment" here and there, but perhaps you haven't really paid any attention to it. That's alright, because this might be one of the most obscure features of Umbraco.
ℹ️ NOTE |
---|
In this post, I have some sharp critique on segments as a feature. Keep in mind that this critique is based on my experience in Umbraco 10.6 and that it may not adequately represent newer major versions of Umbraco. |
Introduction to segments
So what are segments? Segments are just like languages: different versions of the same content for different audiences. In my use case, I had to allow the content editor to explicitly define different target audiences. For each target audience, they wanted to optionally replace the content with a personalised version to better suit the needs of their large audience. Each visitor would then be allowed to choose their own target audience and see the information that was relevant to them.
Getting them to work
Let me be straight: it was not fun, trying to work this out. As it stands, this feature is pretty much designed by uMarketingSuite, for uMarketingSuite. As with any niche feature, the lack of documentation on this was all too familiar, but the sight of a blogpost with step-by-step instructions was a relief... or so I thought.
I got far enough into this tutorial to give me the confidence that I could actually pull this of, until I got to the point of actually creating the segment. "Just do the DeepClone on the variation object", is what the tutorial says. Well, that method no longer exists. So here I am, manually deep cloning the variation object. A little tedious, but doable.
Once this was done, I gave it a spin... and obviously it wasn't working. I don't think I've ever seen as funky behaviour in Umbraco as I saw now, with no real rhyme or reason to it. Publishing one variant caused other variants to change, they might save, they might publish, they might do nothing. With no documentation to back me up, I was left to trial-and-error, but I eventually got something to work.
So what did I get to work?
- ✅ I can enable segments on a document type
- ✅ I can enable segmentation on the properties of document types
- ✅ Content editors can create new variations based on their own defined target audiences
- ❌ I cannot publish one variation without publishing all the others as well
- ❌ I cannot unpublish a specific variation without unpublishing them all
- ❌ I cannot gracefully fall back on the default version if no variation exists for the specified target audience.
- ❌ I cannot remove a variation if I don't want it anymore, it's stuck there
- ❌ I cannot remove a variation if the intended target audience no longer exists
Most of the red crosses here are a direct consequence of how Umbraco works and cannot be fixed. Others would simply require so much work that it's simply not worth the effort.
In case you still want to work with segments
You can treat this chapter as an amendment to the blog post that explains them. The blog post is a good place to get started, but you can take inspiration from here when you get stuck.
Enabling segmentation on document types
The blog just enables segments on all document types. Cool, but I want to decide for myself which document types can have segments. You could be like me and, inspired by the blogpost, create a little button in the document type menu yourself to enable or disable segments. You could also be clever and check the source code, because secretly it's already there:
This screenshot was taken from the DevTools while I was logged in to Umbraco and while I was editing a document type. You can clearly see that something related to segments has been hidden there.
If we check the source code, we find that there is a line that enables this element by simply reading a flag from the ServerVariables. It has been cleverly hidden, because you can't find this flag if you just read the server variables inside your DevTools. A small piece of code can bring the toggle into view:
public class VariantsServerVariablesParser : INotificationHandler<ServerVariablesParsingNotification>
{
public void Handle(ServerVariablesParsingNotification notification)
{
if (!notification.ServerVariables.TryGetValue("umbracoSettings", out object? umbracoSettingsObject)
|| umbracoSettingsObject is not IDictionary<string, object> umbracoSettings)
{
umbracoSettings = new Dictionary<string, object>();
notification.ServerVariables.Add("umbracoSettings", umbracoSettings);
}
umbracoSettings["showAllowSegmentationForDocumentTypes"] = true;
}
}
public class VariantsComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<ServerVariablesParsingNotification, VariantsServerVariablesParser>();
}
}
Now take a look at your document type settings and notice the extra toggle:
Creating segments
The blog doesn't really go into detail about what properly configured segments look like. It also uses a method .DeepClone()
, which doesn't exist anymore. Instead, you have to deep clone the object manually. You have to keep in mind that not all values can be copied over just like that, some values need to get a specific value after copying. Here is the code that I used to create my new variants:
// Only create segments if they don't exist yet. After saving and publishing, all variants likely already exist on the document type, so you don't want to add them again.
// Notice that `segments` here is whatever you make it. For me it's a list of names and ids, passed in from some external code.
foreach (var segment in segments.Where(s => !newVariants.Any(v => v.Language?.Id == variant.Language?.Id && v.Segment == s.Id)))
{
var newVariant = new ContentVariantDisplay
{
AllowedActions = new List<string>(variant.AllowedActions),
DisplayName = segment.Name,
Language = variant.Language,
Segment = segment.Id,
State = ContentSavedState.NotCreated, // 👈 State has to be 'NotCreated'
Tabs = variant.Tabs.Select(x => new Tab<ContentPropertyDisplay>
{
Alias = x.Alias,
IsActive = x.IsActive,
Expanded = x.Expanded,
Id = x.Id,
Key = x.Key,
Label = x.Label,
Type = x.Type,
Properties = x.Properties?.Select(p => new ContentPropertyDisplay
{
Alias = p.Alias,
Config = p.Config,
ConfigNullable = p.ConfigNullable,
Culture = p.Culture,
DataTypeKey = p.DataTypeKey,
Description = p.Description,
Editor = p.Editor,
HideLabel = p.HideLabel,
Id = p.Id,
IsSensitive = p.IsSensitive,
Label = p.Label,
LabelOnTop = p.LabelOnTop,
PropertyEditor = p.PropertyEditor,
Readonly = p.Readonly,
Segment = variantProps.Contains(p.Alias) ? segment.Id : null, // 👈 This value must be set to the segment, but only if this property varies by segment.
SupportsReadOnly = p.SupportsReadOnly,
Validation = p.Validation,
Value = variantProps.Contains(p.Alias) ? null : p.Value, // 👈 The value must only be copied if the property is not variant by segment
Variations = p.Variations,
View = p.View,
}),
})
};
additionalVariants.Add(newVariant);
}
So now we have our segments. Our work is done, right? Wrong! This still doesn't work. Internally, Umbraco relies on the order of all variations, so if the order is not consistent, your backoffice gets messed up. You can solve this, simply by sorting your variations before serving them to the frontend:
notification.Content.Variants = newVariants
.OrderBy(v => IsDefaultLanguage(v) ? 0 : 1)
.ThenBy(v => IsDefaultSegment(v) ? 0 : 1)
.ThenBy(v => v?.Language?.Name)
.ThenBy(v => v.Segment)
.ToList();
private static bool IsDefaultLanguage(ContentVariantDisplay variant) =>
variant.Language == null || variant.Language.IsDefault;
private static bool IsDefaultSegment(ContentVariantDisplay variant) => variant.Segment == null;
Now finally, we have something that works... somewhat.
The blog proceeds to set the variation context inside a render controller. That, however, is rather unrealistic. If you're serious about segments, you'll create something that you can actually reuse, like an action filter or a middleware:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class SegmentAttribute : Attribute, IFilterFactory
{
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return ActivatorUtilities.CreateInstance<InternalSegmentActionFilter>(serviceProvider);
}
private sealed class InternalSegmentActionFilter : IActionFilter
{
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
public InternalSegmentActionFilter(IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor)
{
_variationContextAccessor = variationContextAccessor;
_umbracoContextAccessor = umbracoContextAccessor;
}
public void OnActionExecuted(ActionExecutedContext context)
{
// Method not needed, but part of the interface
}
public void OnActionExecuting(ActionExecutingContext context)
{
// Use the request context to find out which segment the visitor is in. My `.GetSegment()` extension method uses the query string
var segment = context.HttpContext.GetSegment();
var currentPage = _umbracoContextAccessor.GetRequiredUmbracoContext().PublishedRequest.PublishedContent
?? throw new InvalidOperationException("The segment action filter can only be used on actions that are related to an umbraco page");
// You need to find out if the current content actually varies by segment and by the exact segment of the user, because if you assign a segment that does not exist, you'll get an empty page!
// there is no proper way to check if a page varies by segment, other than enumerating all properties and check if any of them vary by segment AND at least one of the variant properties has a value.
// You'll likely need to check on your culture as well, this example assumes that the culture is null.
var segmentShouldBeApplied = currentPage.Properties.Any(p => p.PropertyType.VariesBySegment() && p.HasValue(null, segment.Id));
if (segmentShouldBeApplied)
{
var variationContext = new VariationContext(_variationContextAccessor.VariationContext?.Culture, segment.Id);
_variationContextAccessor.VariationContext = variationContext;
}
}
}
}
In hindsight
I'm disappointed by Umbraco's segmentation feature. It really only works if you have a mature custom management platform behind it (like uMarketingSuite) and it severly lacks in the developer experience, which is very unlike Umbraco. That being said, the extreme lack of documentation would suggest that segments isn't a feature targeted at consumers, but more for internal use. This idea, however, seems contradicted by the fact that Umbraco internally doesn't use segments at all and the fact that Umbraco explicitly posts about it in their own blog. Make of that what you will.
That concludes my experience with segmentation in Umbraco. I hope this was helpful to you and I'll see you in my next blog! 😊
Posted on September 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.