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)
{
}
}
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)
{ }
}
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 fullIUrlProvider
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);
}
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>();
}
}
Alternatively, if your solution has multiple providers, the provider could be inserted before a specific provider:
builder.UrlProviders().InsertBefore<DefaultUrlProvider, BlogPostUrlProvider>();
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>();
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.
Handling the inbound request
Umbraco will go through all registered IContentFinder
s until one of them returns true, after which no further IContentFinder
s 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 IContentFinder
s.
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)
{
}
}
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);
}
Register the BlogPostContentFinder
:
public class BlogPostComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.UrlProviders().Insert<BlogPostUrlProvider>();
builder.ContentFinders().Append<BlogPostContentFinder>();
}
}
Build your solution and publish a blog post and everything should work:
Top comments (0)