Implementing a custom UrlProvider in Umbraco and its pitfalls

Posted on Thursday, March 16, 2017

In this tutorial, I will show you how to implement a custom UrlProvider in Umbraco based on of our latest projects we build. I will explain why we had to create a custom UrlProvider, I will guide you step by step in implementing one and I will explain what the pitfalls are.

 

The project content structure

First, I will show you a preview about how our content is structured in the Umbraco backoffice.

As you can see we have a Home node, some other nodes like a Contact page and we have 2 containers called Categorie and Handelaar*.

*For people that aren’t Dutch, Categorie means Category ( no way! ) and Handelaar means Company.

These 2 containers are Document Types without a Template, why? Because we don’t want these nodes to be visible in our frontend website. The only reason why we have created these nodes is only for containing elements.

 

The Category node

For the Category node, we had to make sure that the customer can add level one primary categories and level 2 subcategories, as shown below.

 

The Company node

Our other container, Company, is set as a listview because then it will be easier for the customer to filter and have a better overview about the added companies.

 

Why implementing a custom UrlProvider?

When using Umbraco, the url for each content node is built upon the content structure. Umbraco starts by taking the current node and then goes up until he reaches the root node.

 

If we have a look at the Category container and take one of our primary categories, you can see the url is presented as /categorie/juwelen-en-accessoires/. The same counts if we take on of our subcategories, which will have an url as /categorie/juwelen-en-accessoires/horloges/.

 

This looks good, but for SEO the categorie segment doesn’t have anything to do with ‘juwelen en accessoires’ so we need to remove this segment from the url. Also, we wanted to remove the primary category segment from the url of our subcategories. So, we want to have a result which looks like /juwelen-en-accessoires/ for our primary category and /horloges/ for our subcategory.

 

Now for our companies we have 2 different types of urls we want to use. The first url will be like the categories and is just the urlName of the company like /company-1/. The second valid url will be built using the subcategory a company is linked at, like for example /horloges/company-1/.

 

To make this work we need to create a custom UrlProvider, one for our categories and one for our companies.

 

Create our custom UrlProviders

The first step is to create a new class called CategoriesUrlProvider which implements the IUrlProvider interface.


    ///
    /// Implements a CategoriesUrlProvider to define a custom url for categories
    ///
    public class CategoriesUrlProvider : IUrlProvider
    {
        public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
        {
            return Enumerable.Empty();
        }

        ///
        /// Get the correct url for Category doctypes ( Primary and sub )
        ///
        public string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode)
        {
            // Take curent content item from cache by its id
            var content = umbracoContext.ContentCache.GetById(id);

            // Check if the content exists and it has alias companyItem
            if (content != null && (content.DocumentTypeAlias == "primaryCategory" || content.DocumentTypeAlias == "subCategory"))
            {
                // Build path
                string route = "/" + content.UrlName + "/";

return route; } return null; } }

 

In the GetUrl method we will take the current node from the cache, check if it’s a primary or subcategory document type and set the new url as the urlName of the category. This will remove all parent segments from the url.

We can do the same for our Companies, create a new class CompaniesUrlProvider and implement the IUrlProvider interface.


    ///
    /// Implements a CompaniesUrlProvider to define a custom url for companies
    ///
    public class CompaniesUrlProvider : IUrlProvider
    {
        public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
        {
            return Enumerable.Empty();
        }

        ///
        /// Get the correct url for Company Item doctypes
        ///
        public string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode)
        {
            // Take curent content item from cache by its id
            var content = umbracoContext.ContentCache.GetById(id);

            // Check if the content exists and it has alias companyItem
            if (content != null && (content.DocumentTypeAlias == "companyItem"))
            {
                // Build path
                string route = "";

                // If requested url is coming from a sub category page then add this sub category as 
                // a segment in the url before the company
                var currentPage = umbracoContext.PublishedContentRequest != null ? 
                    umbracoContext.PublishedContentRequest.PublishedContent : null;

                if (currentPage != null && currentPage.DocumentTypeAlias == "subCategory")
                {
                    route = "/" + currentPage.UrlName.Replace("ë", "e") + "/" + content.UrlName + "/";
                } else
                {
                    route = "/" + content.UrlName + "/";
                }

                return route;
            }

            return null;
        }
    }

 

Reminder I mentioned about serving 2 types of urls for our companies? This is done in the GetUrl method based on the current page we are. If we building the url for a company when we are on a subcategory page, then we will add the subcategory urlName as a segment, else we will serve just the company urlName.

The last thing that we have to do is register our UrlProviders. This can be done by creating a new class called AppEvents which implements the ApplicationEventHandler as shown below:


    public class AppEvents : ApplicationEventHandler
    {
        /// 
        /// Event occures when application is starting
        /// 
        protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            /// Inserts the  before the 
            UrlProviderResolver.Current.InsertTypeBefore<DefaultUrlProvider, CategoriesUrlProvider>();

            /// Inserts the  before the 
            UrlProviderResolver.Current.InsertTypeBefore<DefaultUrlProvider, CompaniesUrlProvider>();
        }
    }

 

Congratz! You have successfully implemented your first custom UrlProvider! If you go to the backoffice and look at the url of the categories or companies you will see the new url.

 

Primary category:

 

Subcategory:

 

Company

 

 

But wait, there are some pitfalls we need to solve which we will now explain in the next part.

 

Pitfalls with the custom UrlProvider

While we were happy with our custom urls, we found an issue that we needed to solve before going any further. On our Masterpage we have a link tag which contains the canonical url of a page and contains the following markup:

@Umbraco.NiceUrlWithDomain(CurrentPage.Id)

 

So by using this we would expect to see the canonical url for our primary category like http://www.domain.be/juwelen-en-accessoires/, but if we go the our website and view the source we see: /juwelen-en-accessoires/

 

Woops! Where is the rest of our url?

This is basically very simple. While we are serving our custom url provider before the default url provider we only return segments of the url without appending the domain. So we will need to update our UrlProviders to add the domain if requested. Also, reminder that Umbraco also has some configurations that have effect of serving the url. For instance, the useDomainPrefixes and addTrailingSlash settings from the umbracoSettings.config file.

 

Edit our UrlProviders to work completely with Umbraco

For this we needed to go into the Umbraco core and find out how Umbraco serves the urls in the DefaultUrlProvider.

https://github.com/umbraco/Umbraco-CMS/blob/52b1a08912cc43995dd9e112c1b4008e26c4258e/src/Umbraco.Web/Routing/DefaultUrlProvider.cs

By looking at the code we realised we couldn’t use any internal Umbraco classes or interfaces to make this work. Also, the IRequestHandlerSection is internal which contains the value of the configuration settings as mentioned above.

 

So how can we solve this?

 

First, we made a new abstract class called UrlProviderBase, which every custom UrlProvider will inherit from:


    /// 
    /// Implements a UrlProviderBase class
    /// 
    public abstract class UrlProviderBase
    {
        /// 
        /// Assemble the Uri of the company item
        /// 
        protected Uri AssembleUrl(Uri domainUri, string path, Uri current, UrlProviderMode mode)
        {
            Uri uri;

            if (mode == UrlProviderMode.AutoLegacy)
            {
                // Normally we need to check the UseDomainPrefixes property in umbraco settings.
                // But because we can't access it here, we set it directly to 'Auto'.
                mode = UrlProviderMode.Auto;
            }

            if (mode == UrlProviderMode.Auto)
            {
                if (current != null && domainUri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority))
                    mode = UrlProviderMode.Relative;
                else
                    mode = UrlProviderMode.Absolute;
            }

            switch (mode)
            {
                case UrlProviderMode.Absolute:
                    uri = new Uri(CombinePaths(domainUri.GetLeftPart(UriPartial.Path), path));
                    break;
                case UrlProviderMode.Relative:
                    uri = new Uri(CombinePaths(domainUri.AbsolutePath, path), UriKind.Relative);
                    break;
                default:
                    throw new ArgumentOutOfRangeException("mode");
            }

            return UriUtility.UriFromUmbraco(uri);
        }

        /// 
        /// Combine domain and route segments
        /// 
        string CombinePaths(string path1, string path2)
        {
            string path = path1.TrimEnd('/') + path2;
            return path == "/" ? path : path.TrimEnd('/');
        }
    }

 

This base class has code which we took from the Umbraco core and will do all the checks for serving the correct url. You will see that we still can’t call the configuration settings, so we manually set the UrlProviderMode to auto when its AutoLegacy.

 

After adding this class we will update our UrlProviders to inherit from our UrlProviderBase class and return the correct url by calling the AssembleUrl method.

 

 

CategoriesUrlProvider


    public class CategoriesUrlProvider : UrlProviderBase, IUrlProvider
    {
        public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
        {
            return Enumerable.Empty();
        }

        /// 
        /// Get the correct url for Category doctypes ( Primary and sub )
        /// 
        public string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode)
        {
            // Take curent content item from cache by its id
            var content = umbracoContext.ContentCache.GetById(id);

            // Check if the content exists and it has alias companyItem
            if (content != null && (content.DocumentTypeAlias == "primaryCategory" || content.DocumentTypeAlias == "subCategory"))
            {
                // Build path
                string route = "/" + content.UrlName + "/";

                // Setup domain uri
                var domainUri = new Uri(current.GetLeftPart(UriPartial.Authority));

                return AssembleUrl(domainUri, route, current, mode).ToString();
            }

            return null;
        }
    }


CompaniesUrlProvider


    public class CompaniesUrlProvider : UrlProviderBase, IUrlProvider
    {
        public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current)
        {
            return Enumerable.Empty();
        }

        /// 
        /// Get the correct url for Company Item doctypes
        /// 
        public string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode)
        {
            // Take curent content item from cache by its id
            var content = umbracoContext.ContentCache.GetById(id);

            // Check if the content exists and it has alias companyItem
            if (content != null && (content.DocumentTypeAlias == "companyItem"))
            {
                // Build path
                string route = "";

                // If requested url is coming from a sub category page then add this sub category as 
                // a segment in the url before the company
                var currentPage = umbracoContext.PublishedContentRequest != null ? 
                    umbracoContext.PublishedContentRequest.PublishedContent : null;

                if (currentPage != null && currentPage.DocumentTypeAlias == "subCategory")
                {
                    route = "/" + currentPage.UrlName.Replace("ë", "e") + "/" + content.UrlName + "/";
                } else
                {
                    route = "/" + content.UrlName + "/";
                }

                // Setup domain uri
                var domainUri = new Uri(current.GetLeftPart(UriPartial.Authority));

                return AssembleUrl(domainUri, route, current, mode).ToString();
            }

            return null;
        }
    }

 

If everything is set correctly we should now see the correct url on our frontend website for our canonical url. Even when we moved to https a few months later, the canonical url was set correctly.

 

Now you can start by creating a ContentFinder to look after the correct node based on the url, but this I will not be covering is this post.

 

  • umbraco
  • urlprovider
  • contentfinder