I'm not sure, if this might help someone, but I wanted to share my learnings in creating a custom url field in Umbraco v8.
My target was to add a field to the backoffice, which offers the user to provide a custom url for the page. I didn't want to do it generic (e.g. for all pages of a certain directory) but only punctually.
How did I achieve this goal?
Added custom property to document type ("customUrl")
Created CustomUrlProvider which inherits IUrlProvider and CustomUrlFinder which inherits IContentFinder.
The CustomUrlProvider is - as the name already states - responsible for providing the customUrl (if existent):
public class CustomUrlProvider : IUrlProvider
{
public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent page, UrlProviderMode mode, string culture, Uri current)
{
var content = umbracoContext.ContentCache.GetById(page.Id);
if(content != null && content.HasProperty("customUrl"))
{
var url = content.GetProperty("customUrl").ToString();
if(!string.IsNullOrWhiteSpace(url))
{
return new UrlInfo(url, true, "de-DE");
}
}
return null;
}
public IEnumerable<UrlInfo> GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
{
return Enumerable.Empty<UrlInfo>();
}
}
The CustomUrlFinder is searching all descendants of my root page for a matching customUrl-property and returns true if there is a hit:
public class CustomUrlFinder : IContentFinder
{
public bool TryFindContent(PublishedRequest contentRequest)
{
var path = contentRequest.Uri.GetAbsolutePathDecoded();
var root = contentRequest.UmbracoContext.ContentCache.GetAtRoot().FirstOrDefault();
var content = root.Descendants().FirstOrDefault(x => x.Value("customUrl").Equals(path));
if (content == null) return false;
contentRequest.PublishedContent = content;
return true;
}
}
And of course register the component:
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class UpdateContentFindersComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.ContentFinders().InsertBefore<ContentFinderByUrl, CustomUrlFinder>();
}
}
What still is missing: If the request comes with the "original url" (e.g. from umbraco backoffice content app info), the page request results in a 404.
I'm not sure, if I did everything correctly the way it's meant to be, but I haven't found any other way to solve this. Any feedback is greatly apprechiated!
First of all, very nice work - I love that you are sharing it here.
If I may, I do have a few comments on the above code.
CustomUrlProvider.GetUrl
you do a look up for content by the id of the page that is passed in - content will then be the same as page -> you don't need the lookup :)
content.GetProperty("customUrl").ToString() could be written as content.Value<string>("CustomUrl")
doing the HasProperty first, actually does a lookup, as is done in Value(), so it would be faster to just do var customUrl = page.Value<string>("customUrl");
The whole method could then be:
public UrlInfo GetUrl(UmbracoContext umbracoContext, IPublishedContent page, UrlProviderMode mode, string culture, Uri current)
{
var customUrl = page?.Value<string>("customUrl");
if(string.IsNullOrWhiteSpace(customUrl)) return null;
return return new UrlInfo(url, true, "de-DE");
}
CustomUrlFinder.TryFindContent
In the url finder you traverse the whole cache by using .Descendants(). This means that for every request your site gets, all content from the cache is deserialized. In v7 this could be VERY slow, but in v8 the cache works different so this may not be a problem.
I would suggest you do a lookup in examine instead for the url, and use the id from the result, to find the published content from the cache.
public bool TryFindContent(PublishedRequest contentRequest)
{
var path = contentRequest.Uri.GetAbsolutePathDecoded();
var root = contentRequest.UmbracoContext.ContentCache.GetAtRoot().FirstOrDefault();
if(!ExamineManager.Instance.TryGetSearcher(Constants.UmbracoIndexes.ExternalIndexName, out var searcher))
return false;
var quqery = searcher.CreateQuery().Field("customUrl", path);
var results = Umbraco.ContentQuery.Search(q);
var content = results.FirstOrDefault();
if (content == null) return false;
contentRequest.PublishedContent = content;
return true;
}
You may also want to consider the domains, if there are multiple sites in the installation :)
Regarding the problem with overriding the url, returning 404 - if you inherit from the base content finder, instead of returning false, you could return base.TryFindContent
With some additional help from Michael Argentini (https://our.umbraco.com/forum/umbraco-8/96045-where-are-the-examine-config-files-located-in-v8), I've managed to make it work.
The GetUrl() is now looking as you suggested Søren and the TryFindContent() had to be adjusted just a bit, because of two things:
There was no Examine searcher active, so I had to get it from the index
Umbraco.ContentQuery was not available
My method looks now like this:
public bool TryFindContent(PublishedRequest contentRequest)
{
var path = contentRequest.Uri.GetAbsolutePathDecoded();
var root = contentRequest.UmbracoContext.ContentCache.GetAtRoot().FirstOrDefault();
if (!ExamineManager.Instance.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out var index) || !(index is IUmbracoIndex umbIndex)) return false;
var searcher = umbIndex.GetSearcher();
var query = searcher.CreateQuery().Field("customUrl", path);
var result = query.Execute();
if (result == null || result.TotalItemCount == 0) return false;
Int32.TryParse(result.FirstOrDefault().Id, out int pageId);
var content = contentRequest.UmbracoContext.ContentCache.GetById(pageId);
if (content == null) return false;
contentRequest.PublishedContent = content;
return true;
}
Probably still has potential for improvement, but it's working for now. What I didn't get was your suggestion how to solve the 404. As I understand it, there must be some logic to tell umbraco backoffice to either display the new link or redirect to the custom url when the user clicks on it.
Found this thread whilst working on a similar requirement.
There is actually a documentation page which covers this very well for V8 - with examples of custom code and wiring up the custom URL providers with the composer.
I managed to solve my requirement by using UrlSegmentProvider - which is very neat because you don't have to build a corresponding ContentFinder when you are only changing the url of the current node.
Custom URL
I'm not sure, if this might help someone, but I wanted to share my learnings in creating a custom url field in Umbraco v8.
My target was to add a field to the backoffice, which offers the user to provide a custom url for the page. I didn't want to do it generic (e.g. for all pages of a certain directory) but only punctually.
How did I achieve this goal?
Added custom property to document type ("customUrl")
Created CustomUrlProvider which inherits IUrlProvider and CustomUrlFinder which inherits IContentFinder.
The CustomUrlProvider is - as the name already states - responsible for providing the customUrl (if existent):
The CustomUrlFinder is searching all descendants of my root page for a matching customUrl-property and returns true if there is a hit:
And of course register the component:
What still is missing: If the request comes with the "original url" (e.g. from umbraco backoffice content app info), the page request results in a 404.
I'm not sure, if I did everything correctly the way it's meant to be, but I haven't found any other way to solve this. Any feedback is greatly apprechiated!
Sources:
Hi,
First of all, very nice work - I love that you are sharing it here.
If I may, I do have a few comments on the above code.
CustomUrlProvider.GetUrl
content.GetProperty("customUrl").ToString()
could be written ascontent.Value<string>("CustomUrl")
var customUrl = page.Value<string>("customUrl");
The whole method could then be:
CustomUrlFinder.TryFindContent
In the url finder you traverse the whole cache by using .Descendants(). This means that for every request your site gets, all content from the cache is deserialized. In v7 this could be VERY slow, but in v8 the cache works different so this may not be a problem.
I would suggest you do a lookup in examine instead for the url, and use the id from the result, to find the published content from the cache.
You may also want to consider the domains, if there are multiple sites in the installation :)
Thank you for the valuable feedback! That's why I love this community :) I'm going to give your suggestions a try and comment the results!
Beware: they are not tested!
Looking forward to hear if they helped :-)
Regarding the problem with overriding the url, returning 404 - if you inherit from the base content finder, instead of returning false, you could return base.TryFindContent
With some additional help from Michael Argentini (https://our.umbraco.com/forum/umbraco-8/96045-where-are-the-examine-config-files-located-in-v8), I've managed to make it work. The GetUrl() is now looking as you suggested Søren and the TryFindContent() had to be adjusted just a bit, because of two things:
My method looks now like this:
Probably still has potential for improvement, but it's working for now. What I didn't get was your suggestion how to solve the 404. As I understand it, there must be some logic to tell umbraco backoffice to either display the new link or redirect to the custom url when the user clicks on it.
Could you show how you registered CustomUrlProvider? I am doing this for the first time in v8. It used to be in ApplicationEventHandler:
Hey Organic,
The ApplicationEventHandler no longer exists in Umbraco v8. Instead you need to use a Composer and a Component.
https://our.umbraco.com/documentation/Implementation/Composing/
That should hopefully get you started :-)
Thanks
Nik
Yes, thank you, I read about that. My question was about how to register the provider. This seems to work:
Found this thread whilst working on a similar requirement.
There is actually a documentation page which covers this very well for V8 - with examples of custom code and wiring up the custom URL providers with the composer.
https://our.umbraco.com/documentation/reference/routing/request-pipeline/outbound-pipeline
I managed to solve my requirement by using UrlSegmentProvider - which is very neat because you don't have to build a corresponding ContentFinder when you are only changing the url of the current node.
is working on a reply...