Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • mcgrph 35 posts 162 karma points
    Apr 25, 2019 @ 21:16
    mcgrph
    2

    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):

    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!

    Sources:

  • Søren Gregersen 441 posts 1884 karma points MVP 2x c-trib
    Apr 25, 2019 @ 22:14
    Søren Gregersen
    2

    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

    • 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 :)

  • mcgrph 35 posts 162 karma points
    Apr 26, 2019 @ 06:38
    mcgrph
    1

    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!

  • Søren Gregersen 441 posts 1884 karma points MVP 2x c-trib
    Apr 26, 2019 @ 07:04
    Søren Gregersen
    0

    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

  • mcgrph 35 posts 162 karma points
    Apr 27, 2019 @ 20:24
    mcgrph
    100

    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:

    1. There was no Examine searcher active, so I had to get it from the index
    2. 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.

  • organic 108 posts 157 karma points
    Aug 05, 2019 @ 20:04
    organic
    0

    Could you show how you registered CustomUrlProvider? I am doing this for the first time in v8. It used to be in ApplicationEventHandler:

        protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            UrlProviderResolver.Current.InsertTypeBefore<DefaultUrlProvider, CustomUrlProvider>();
        }
    
  • Nik 1593 posts 7151 karma points MVP 6x c-trib
    Aug 05, 2019 @ 21:05
    Nik
    0

    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

  • organic 108 posts 157 karma points
    Aug 06, 2019 @ 15:29
    organic
    0

    Yes, thank you, I read about that. My question was about how to register the provider. This seems to work:

    composition.UrlProviders().InsertBefore<DefaultUrlProvider, ExternalUrlProvider>();
    
  • Alan Mitchell 57 posts 279 karma points c-trib
    Oct 09, 2019 @ 11:08
    Alan Mitchell
    1

    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.

Please Sign in or register to post replies

Write your reply to:

Draft