DEV Community

Cover image for Routing in Umbraco Part 2: Content Finders
Morten Hartvig
Morten Hartvig

Posted on

Routing in Umbraco Part 2: Content Finders

The previous article in this series demoed how to adjust segments for your Umbraco nodes using an IUrlSegmentProvider. When an altered segment is returned in an IUrlSegmentProvider it becomes a native segment that Umbraco can handle out of the box and no further URL manipulation/rewrite is required.

But.. what if you wanted to change the path? Say, for example, you have a blog and you want to automatically have the year of the blog post's creation inserted in the URL, such as /blog/hello-world should become /blog/2024/hello-world. This is where an IContentFinder comes in.

When you alter part of a node's path and not simply the segment, Umbraco will need some help mapping the inbound request to the node, as the new path assigned to the node is not natively known by Umbraco.

This article will show how to provide a new path for a node and how to return that node for the inbound request.

Changing the path

Much like an IUrlSegmentProvider Umbraco has an IUrlProvider that can be implemented for change the URL for your nodes.

The interface has two methods: GetUrl() and GetOtherUrls(). The latter will let your return other URLs to display in the Backoffice on the node's Info Content App.

Below is an example for GetUrl().

public class BlogPostUrlProvider : IUrlProvider
{
    public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
    {

    }

    public IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

However, instead of implementing IUrlProvider, Umbraco ships with a DefaultUrlProvider with a virtual method for both GetOtherUrls() and GetUrl() that can be overwritten.

public class BlogPostUrlProvider : DefaultUrlProvider
{
    public BlogPostUrlProvider(IOptionsMonitor<RequestHandlerSettings> requestSettings, 
        ILogger<DefaultUrlProvider> logger, 
        ISiteDomainMapper siteDomainMapper, 
        IUmbracoContextAccessor umbracoContextAccessor, 
        UriUtility uriUtility, 
        ILocalizationService localizationService) : base(requestSettings, logger, siteDomainMapper, umbracoContextAccessor, uriUtility, localizationService)
    { }
}
Enter fullscreen mode Exit fullscreen mode

Note that ILocalizationService is removed in V15. Unfortunately DefaultUrlProvider does not yet have a constructor that accepts ILanguageService. If you want to skip changing the injected interface as part of a V15 upgrade, consider doing a full IUrlProvider implementation. Inspiration can be found on Github.

Change the blog post's UrlInfo in GetUrl() like so:

public override UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
{
    if (content.ContentType.Alias != "blogPost") return null;

    var segment = content.UrlSegment;
    if (segment is null) return null;

    var currentUrlInfo = base.GetUrl(content, mode, culture, current);
    if (currentUrlInfo is null) return null;

    // To add the post's created year one could 
    // simply replace the post's segment in the 
    // URL with year/segment.
    var currentUrl = currentUrlInfo.Text;
    var customUrl = currentUrl.Replace(segment,
        $"{content.CreateDate.Year}/{segment}");

    return new UrlInfo(customUrl, true, culture);
}
Enter fullscreen mode Exit fullscreen mode

Register the BlogPostUrlProvider. Insert<T>() accepts an int index. When no index is provided it defaults to 0.

public class BlogPostComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.UrlProviders().Insert<BlogPostUrlProvider>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, if your solution has multiple providers, the provider could be inserted before a specific provider:

builder.UrlProviders().InsertBefore<DefaultUrlProvider, BlogPostUrlProvider>();
Enter fullscreen mode Exit fullscreen mode

The following methods are available for registering your provider. The same goes for ContentFinders():

builder.UrlProviders().Append<T>();
builder.UrlProviders().Insert<T>(int index = 0);
builder.UrlProviders().Remove<T>();
builder.UrlProviders().InsertAfter<TAfter,T>();
builder.UrlProviders().InsertBefore<TBefore, T>();
Enter fullscreen mode Exit fullscreen mode

If you attempt to view the URL in the Backoffice you will notice that it is not working. This is because we have yet to change the inbound request.

Blog post with broken URL

Handling the inbound request

Umbraco will go through all registered IContentFinders until one of them returns true, after which no further IContentFinders are executed.

The goal of an IContentFinder is therefore to set an IPublishedContent for the request and return true, so Umbraco can load the node and stop executing the rest of the IContentFinders.

Create a BlogPostContentFinder that implements IContentFinder and inject IUmbracoContextAccessor.

public class BlogPostContentFinder : IContentFinder
{
    private readonly IUmbracoContextAccessor _contextAccessor;

    public BlogPostContentFinder(IUmbracoContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public Task<bool> TryFindContent(IPublishedRequestBuilder request)
    { 

    }
}
Enter fullscreen mode Exit fullscreen mode

In TryFindContent() attempt to find a content node by (in this case) the route. Provided there is a match, set it as the published content and return true.

public Task<bool> TryFindContent(IPublishedRequestBuilder request)
{ 
    // Get the path for the current request
    var path = request.Uri.GetAbsolutePathDecoded();

    // In this example site there will not be other URLs using four digits
    // between two slashes, so they can easily be removed safely like so.
    // E.g. /blog/2024/hello-world -> /blog/hello-world
    var url = Regex.Replace(path, "/[0-9]+/", "/");

    if (!_contextAccessor.TryGetUmbracoContext(out var context)) return Task.FromResult(false);

    // Attempt to find the content
    var content = context.Content?.GetByRoute(url);
    if (content is null) return Task.FromResult(false);

    // Set it as the published content for the request
    request.SetPublishedContent(content);

    // Return true to let Umbraco know a match was found
    return Task.FromResult(true);
}
Enter fullscreen mode Exit fullscreen mode

Register the BlogPostContentFinder:

public class BlogPostComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.UrlProviders().Insert<BlogPostUrlProvider>();
        builder.ContentFinders().Append<BlogPostContentFinder>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Build your solution and publish a blog post and everything should work:

Blog post with updated URL in Umbraco

Blog post with updated URL in the frontend

Top comments (0)