ContentFinder in Umbraco 8
Heads Up!
This article is several years old now, and much has happened since then, so please keep that in mind while reading it.
Like most CMS platforms Umbraco presents a treeview of the content that makes up a website that shows the direct mapping of how each page of content will be shown publically on the website.
For example the root node is most likely accessed at https://www.website.com/ and a child page would be available on https://www.website.com/child
The simple hierarchy a treeview presents is easily understood by most people without explanation and serves the needs of many websites.
However there are some cases when purely relying on a rigid hierarchy of content which maps exactly to the published content can become hard to manage.
I worked on a project for Greene King which had a requirement to create multiple child pages for each of their 1,500 or so pubs scattered across a variety of brands like Hungry Horse or Chef & Brewer.
Greene King regularly run campaigns and promotions across a large number of their pubs, resulting in a need to add pages of content that will appear in several places.
The most basic way to approach this need would be to create a single campaign page of content and then copy and paste it under the other pubs it needs to appear on.
Although this approach will produce the desired results, it comes at a high setup and maintenance cost. It’s going to take a long time to copy the content into multiple pages and then even longer to make any edits that may be required in the future.
Thankfully Umbraco provides a better way of dealing with situations like this through the use of a custom Content Finder.
The concept and implementation is deceptively simple for something that can introduce an extreme amount of flexibility on how website content is served up.
For a standard Umbraco installation whenever a public request comes in Umbraco uses it’s own default Content Finder to determine which content should be returned for the request.
This is typically as simple as a request for https://www.website.com/about-us/directions returning the About us > Directions content node. If a request cannot be matched to content, then a 404 will be returned.
However this native process can be augmented through the introduction of custom Content Finders. Once created they can be inserted into the request pipeline to retrieve content based upon custom logic.
If a custom Content Finder does not return any content, Umbraco will let the request fall through into the next Content Finder until it will finally be handled by the default Umbraco Content Finder.
Writing your own custom Content Finder is very straightforward and the official documentation provides a good explanation and simple example.
To demonstrate how this works on a real website I’ve installed a fresh Umbraco 8 site including the default starter kit.
If we navigate to the Products page we’ll see a number of Umbraco related goodies listed out
We want to extend the starter kit so that a page exists underneath every product with information about the shipping costs specifically for that product. Rather than create a physical copy of the shipping page under each product node, we’re going to use a custom Content Finder to clone a single page of content.
Within the back-office I’ve created a new document type that acts purely as a container for our virtual content. In the following screenshot you can see a node called Virtual product child page which is of document type VirtualContent.
Under this I’ve allowed content nodes of type Content Page to be added. This is an existing document type within the starter kit.
We can now create a new page of content called Shipping costs. This is the page of content we’d like returned if requested under each product. E.g. /products/biker-jacket/shipping-costs
For this to happen we need to create our custom Content Finder so that Umbraco will orchestrate this non-stock approach for this type of request.
The code below is commented to explain what is happening.
using System;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Services;
using Umbraco.Web;
using Umbraco.Web.Routing;
namespace Umbraco_8.ContentFinder
{
public class DemoContentFinder : IContentFinder
{
private readonly IContentTypeService _contentTypeService;
public DemoContentFinder(IContentTypeService contentTypeService)
{
_contentTypeService = contentTypeService;
}
public bool TryFindContent(PublishedRequest request)
{
// Abort if not enough URL segments found
if (request.Uri.Segments.Length <= 1)
{
return false;
}
var virtualSegment = request.Uri.Segments.Last();
var parentUri = request.Uri.Segments.Take(request.Uri.Segments.Length - 1);
var parent = request.UmbracoContext.Content.GetByRoute(string.Join("/",parentUri));
// Abort if parent of requested content is not of type "Product"
if (!parent.ContentType.Alias.Equals("product", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
// Find the virtual content container (This holds all our virtual content)
var virtualContentType = request.UmbracoContext.Content.GetContentType("virtualContent");
var virtualContent = request.UmbracoContext.Content.GetByContentType(virtualContentType).FirstOrDefault();
if (virtualContent == null)
{
return false;
}
// Find the virtual content
var content = virtualContent.Descendants().FirstOrDefault(x =>
x.IsPublished() && x.UrlSegment.Equals(virtualSegment, StringComparison.InvariantCultureIgnoreCase));
if (content == null)
{
return false;
}
// Finally return the virtual content
request.PublishedContent = content;
request.TrySetTemplate(content.GetTemplateAlias());
return true;
}
}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class UpdateContentFindersComposer : IUserComposer
{
public void Compose(Composition composition)
{
// Add our custom MyContentFinder just before the core ContentFinderByUrl...
composition.ContentFinders().InsertBefore<ContentFinderByUrl, DemoContentFinder>();
}
}
}
In essence there are two underlying parts to the code above.
The first is that we’ve created some logic to identify when an incoming request is for a child of a products page. If so, we find and return the appropriate page of virtual content.
The second is we've registered our custom Custom Finder, which in our case is a single line to instruct Umbraco to use it for incoming requests before it’s own default Content Finder.
Now if we try our request to /products/biker-jacket/shipping-costs we see the following.
Success, we are seeing an abstract page of content that doesn’t actually exist in the content tree in that position.
I’ve put together quite a contrived example for this article that makes use of the Umbraco 8 starter kit, mostly as a way to make trying to present using a custom Content Finder in an approachable way.
We've only scratched the surface of what can be achieved through custom Content Finders. I mentioned at the start of this article that I’ve successfully used this approach for a number of websites operated by Greene King to enable their editors to much more easily manage content at scale without duplication.
For that project we extended the concept further with abilities like filtering. This means that a piece of virtual content can be configured to only be shown in specific places across the website, rather than always being shown as a child of certain content.
We also added tokenisation into Umbraco so that a page of virtual content could contain tokens that would be replaced when it was rendered. This means that information about the parent page can be pulled in to further customise the experience based upon the context of where the content is being viewed.
Hopefully this article has given you some idea about what is possible through the use of custom Content Finders and the type of approaches it can unlock. Stay abstract!
Peter Bridger
Peter is on Twitter as @PeterBridger