All pages
Powered by GitBook
1 of 6

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Inbound request pipeline

How the Umbraco inbound request pipeline works

The inbound process is triggered by UmbracoRouteValueTransformer and then handled with the Published router. The published content request preparation process kicks in and creates a PublishedRequestBuilder which will be used to create a PublishedContentRequest.

The PublishedContentRequest object represents the request which Umbraco must handle. It contains everything that will be needed to render it. All this occurs when the Umbraco modules knows that an incoming request maps to a document that can be rendered.

public class PublishedContentRequest
{
  public Uri Uri { get; }
  …
}

There are 3 important properties, which contains all the information to find a node:

public bool HasDomain { get; }
public DomainAndUri Domain { get; }
public CultureInfo Culture { get; }

Domain is a DomainAndUri object that is a standard Domain plus the fully qualified uri. For example, the Domain may contain "example.com" whereas the Uri will be fully qualified for example "https://example.com/".

It contains the content to render:

Contains template information:

The published request is created using the PublishedRequestBuilder, which implements IPublishedRequestBuilder. It's only in this builder that it's possible to set values, such as domain, culture, published content, redirects, and so on.

You can subscribe to the 'routing request' notification, which is published right after the PublishedRequestBuilder has been prepared, but before the request is built, and processed. Here you can modify anything in the request before it is built and processed! For example content, template, etc:

public bool HasPublishedContent { get; }
public IPublishedContent PublishedContent { get; set; }
public bool IsInternalRedirect { get; }
public bool IsRedirect {get; }
public bool HasTemplate { get; }
public string GetTemplateAlias { get; }
public ITemplate Template {get; }
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;

namespace Umbraco9.NotificationHandlers;

public class PublishedRequestHandler : INotificationHandler<RoutingRequestNotification>
{
    public void Handle(RoutingRequestNotification notification)
    {
        var requestBuilder = notification.RequestBuilder;
        // Do something with the IPublishedRequestBuilder here 
    }
}

FindPublishedContentAndTemplate()

The followed method is called on the "PublishedContentRequest.PrepareRequest()" method: FindPublishedContentAndTemplate(). We discuss shortly what this method is doing:

  1. FindPublishedContent ()

  2. Handles redirects

  3. HandlePublishedContent()

  4. FindTemplate()

  5. FollowExternalRedirect()

  6. HandleWildcardDomains()

HandlePublishedContent

  • No content?

  • Run the LastChanceFinder

  • Is an IContentFinder, resolved by ContentLastChanceFinderResolver

  • By default, is null (= ugly 404)

FindTemplate

  • Use altTemplate if

  • Initial content

  • Internal redirect content, and InternalRedirectPreservesTemplate is true

  • No alternate template?

FollowExternalRedirect()

  • Content.GetPropertyValue("umbracoRedirect")

  • If it’s there, sets the published content request to redirect to the content

  • Will trigger an external (browser) redirect

HandleWildcardDomains()

  • Finds the deepest wildcard domain between

  • Domain root (or top)

  • Request’s published content

  • If found, updates the request’s culture accordingly

This implements separation between hostnames and cultures

Follow internal redirects

  • Take care of infinite loops

  • Ensure user has access to published content

  • Else redirect to login or access denied published content

  • Loop while there is no content

  • Take care of infinite loops

  • Use the current template if one has already been selected

  • Else use the template specified for the content, if any

  • Alternate template?

    • Use the alternate template, if any

    • Else use what’s already there: a template, else none

  • Alternate template is used only if displaying the intended content

    • Except for internal redirects

    • If you enable InternalRedirectPreservesTemplate

    • Which is false by default

  • Alternate template replaces whatever template the finder might have set

    • ContentFinderByNiceUrlAndTemplate

    • /path/to/page/template1?altTemplate=template2  template2

  • Alternate template does not falls back to the specified template for the content

    • /path/to/page?altTemplate=missing  no template

    • Even if the page has a template

  • But preserves whatever template the finder might have set

    • /path/to/page/template1?altTemplate=missing  template1

  • Routing in Umbraco

    What the Umbraco Request Pipeline is

    This section describes what the Umbraco Request Pipeline is. It explains how Umbraco matches a document to a given request and how it generates a URL for a document.

    Request pipeline

    What is the pipeline

    The request pipeline is the process of building up the URL for a node and resolving a request to a specified node. It ensures that the right content is sent back.

    Outbound vs Inbound

    The pipeline works bidirectional: and .

    is the process of building up a URL for a requested node. is every request received by the web server and handled by Umbraco.

    Customizing the pipeline

    This section will describe the components that you can use to modify Umbraco's request pipeline: & IUrlProvider

    inbound
    outbound
    Outbound
    Inbound
    IContentFinder
    what is the pipeline

    IContentFinder

    Information about creating your own content finders

    To create a custom content finder, with custom logic to find an Umbraco document based on a request, implement the IContentFinder interface:

    and use either an Umbraco builder extension, or a composer to add it to it to the ContentFindersCollection.

    Umbraco runs all content finders in the collection 'in order', until one of the IContentFinders returns true. Once this occurs, the request is then handled by that finder, and no further IContentFinders are executed. Therefore the order in which ContentFinders are added to the ContentFinderCollection is important.

    The ContentFinder can set the PublishedContent item for the request, or template or even execute a redirect.

    Example

    This IContentFinders will find a document with id 1234, when the Url begins with /woot.

    Adding and removing IContentFinders

    You can use either an extension on the Umbraco builder or a composer to access the ContentFinderCollection and add or remove specific ContentFinders.

    Learn more about registering dependencies and when to use which method in the article.

    Umbraco builder extension

    First create the extension method:

    Then invoke in the Program.cs file:

    Composer

    NotFoundHandlers

    To set your own 404 finder create an IContentLastChanceFinder and set it as the ContentLastChanceFinder. (perhaps you have a multilingual site and need to find the appropriate 404 page in the correct language).

    A IContentLastChanceFinder will always return a 404 status code. This example creates a new implementation of the IContentLastChanceFinder and gets the 404 page for the current language of the request.

    You can configure Umbraco to use your own implementation in the Program.cs file:

    When adding a custom IContentLastChanceFinder to the pipeline any Error404Collection-settings in appSettings.json will be ignored.

    public interface IContentFinder
    {
      Task<bool> TryFindContent(IPublishedRequestBuilder contentRequest);
    }
    Dependency Injection
    public class MyContentFinder : IContentFinder
    {
        private readonly IUmbracoContextAccessor _umbracoContextAccessor;
    
        public MyContentFinder(IUmbracoContextAccessor umbracoContextAccessor)
        {
            _umbracoContextAccessor = umbracoContextAccessor;
        }
    
        public Task<bool> TryFindContent(IPublishedRequestBuilder contentRequest)
        {
            var path = contentRequest.Uri.GetAbsolutePathDecoded();
            if (path.StartsWith("/woot") is false)
            {
                return Task.FromResult(false); // Not found
            }
    
            if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext))
            {
                return Task.FromResult(false);
            }
    
            // Have we got a node with ID 1234
            var content = umbracoContext.Content.GetById(1234);
            if (content is null)
            {
                // If not found, let another IContentFinder in the collection try.
                return Task.FromResult(false);
            }
    
            // If content is found, then render that node
            contentRequest.SetPublishedContent(content);
            return Task.FromResult(true);
        }
    }
    using RoutingDocs.ContentFinders;
    using Umbraco.Cms.Core.DependencyInjection;
    using Umbraco.Cms.Core.Routing;
    
    namespace RoutingDocs.Extensions;
    
    public static class UmbracoBuilderExtensions
    {
        public static IUmbracoBuilder AddMyCustomContentFinders(this IUmbracoBuilder builder)
        {
            // Add our custom content finder just before the core ContentFinderByUrl
            builder.ContentFinders().InsertBefore<ContentFinderByUrl, MyContentFinder>();
            // You can also remove content finders, this is not required here though, since our finder runs before the url one
            builder.ContentFinders().Remove<ContentFinderByUrl>();
            // You use Append to add to the end of the collection
            builder.ContentFinders().Append<AnotherContentFinderExample>();
            // or Insert for a specific position in the collection
            builder.ContentFinders().Insert<AndAnotherContentFinder>(3);
            return builder;
        }
    }
    builder.CreateUmbracoBuilder()
        .AddBackOffice()
        .AddWebsite()
        .AddDeliveryApi()
        .AddComposers()
        .AddMyCustomContentFinders()
        .Build();
    using Umbraco.Cms.Core.Composing;
    using Umbraco.Cms.Core.DependencyInjection;
    using Umbraco.Cms.Core.Routing;
    
    namespace RoutingDocs.ContentFinders;
    
    public class UpdateContentFindersComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            // Add our custom content finder just before the core ContentFinderByUrl
            builder.ContentFinders().InsertBefore<ContentFinderByUrl, MyContentFinder>();
            // You can also remove content finders, this is not required here though, since our finder runs before the url one
            builder.ContentFinders().Remove<ContentFinderByUrl>();
            // You use Append to add to the end of the collection
            builder.ContentFinders().Append<AnotherContentFinderExample>();
            // or Insert for a specific position in the collection
            builder.ContentFinders().Insert<AndAnotherContentFinder>(3);
        }
    }
    using System.Linq;
    using System.Threading.Tasks;
    using Umbraco.Cms.Core.Models.PublishedContent;
    using Umbraco.Cms.Core.Routing;
    using Umbraco.Cms.Core.Services;
    using Umbraco.Cms.Core.Web;
    
    namespace RoutingDocs.ContentFinders;
    
    public class My404ContentFinder : IContentLastChanceFinder
    {
        private readonly IDomainService _domainService;
        private readonly IUmbracoContextAccessor _umbracoContextAccessor;
    
        public My404ContentFinder(IDomainService domainService, IUmbracoContextAccessor umbracoContextAccessor)
        {
            _domainService = domainService;
            _umbracoContextAccessor = umbracoContextAccessor;
        }
        
        public Task<bool> TryFindContent(IPublishedRequestBuilder contentRequest)
        {
            // Find the root node with a matching domain to the incoming request
            var allDomains = _domainService.GetAll(true).ToList();
            var domain = allDomains?
                .FirstOrDefault(f => f.DomainName == contentRequest.Uri.Authority
                                        || f.DomainName == $"https://{contentRequest.Uri.Authority}"
                                        || f.DomainName == $"http://{contentRequest.Uri.Authority}");
    
            var siteId = domain != null ? domain.RootContentId : allDomains.Any() ? allDomains.FirstOrDefault()?.RootContentId : null;
    
            if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext))
            {
                return Task.FromResult(false);
            }
    
           if (umbracoContext.Content == null)
                return new Task<bool>(() => contentRequest.PublishedContent is not null);
    
            var siteRoot = umbracoContext.Content.GetById(false, siteId ?? -1);
    
            if (siteRoot is null)
            {
                return Task.FromResult(false);
            }
    
            // Assuming the 404 page is in the root of the language site with alias fourOhFourPageAlias
            var notFoundNode = siteRoot.Children?.FirstOrDefault(f => f.ContentType.Alias == "fourOhFourPageAlias");
    
            if (notFoundNode is not null)
            {
                contentRequest.SetPublishedContent(notFoundNode);
            }
    
            // Return true or false depending on whether our custom 404 page was found
            return Task.FromResult(contentRequest.PublishedContent is not null);
        }
    }
    builder.CreateUmbracoBuilder()
        .AddBackOffice()
        .AddWebsite()
        .AddDeliveryApi()
        .AddComposers()
         // If you need to add something Umbraco specific, do it in the "AddUmbraco" builder chain, using the IUmbracoBuilder extension methods.
        .SetContentLastChanceFinder<RoutingDocs.ContentFinders.My404ContentFinder>()
        .Build();

    Published Content Request Preparation

    How Umbraco prepares content requests

    Is started in UmbracoRouteValueTransformer where it gets the HttpContext and RouteValueDictionary from the netcore framework:

    What it does:

    • It ensures Umbraco is ready, and the request is a document request.

    • Ensures there's content in the published cache, if there isn't it routes to the RenderNoContentController which displays the no content page you see when running a fresh install.

    • Creates a published request builder.

    • Routes the request with the request builder using the PublishedRouter.RouteRequestAsync(…).

      • This will handle redirects, find domain, template, published content and so on.

      • Build the final IPublishedRequest.

    • Sets the routed request in the Umbraco context, so it will be available to the controller.

    • Create the route values with the UmbracoRouteValuesFactory.

      • This is what actually routes your request to the correct controller and action, and allows you to hijack routes.

    • Set the route values to the http context.

    • Handles posted form data.

    • Returns the route values to netcore so it routes your request correctly.

    RouteRequestAsync

    When the RouteRequestAsync method is invoked on the PublishedRouter it will:

    • FindDomain().

    • Handle redirects.

    • Set culture.

    • Find the published content.

    We will discuss a few of these steps below.

    FindDomain()

    The FindDomain method looks for a domain matching the request Uri

    • Using a greedy match: “domain.com/foo” takes over “domain.com”.

    • Sets published content request’s domain.

    • If a domain was found.

      • Sets published content request’s culture accordingly.

    Find published content

    When finding published content the PublishedRouter will first check if the PublishedRequestBuilder already has content, if it doesn't the content finders will kick in. There a many different types of content finders, such as find by url, by id path, and more. If none of the content finders manages to find any content, the request will be set as 404, and the ContentLastChanceFinder will run, this will try to find a page to handle a 404, if it can't find one, the ugly 404 will be used.

    You can also implement your own content finders and last chance finder, for more information, see

    The PublishedRouter will also follow any internal redirects, but it is limited to avoid spiraling out of control due to an infinite redirect loop.

    Find template

    Once the content has been found, the PublishedRouter moves on to finding the template.

    First off it checks if any content was found, if it wasn't it sets the template to null, since there can't be a template without content.

    Next it checks to see if there is an alternative template which should be used. An alternative template will be used if the router can find a value with the key "altTemplate", in either the querystring, form, or cookie, and there is content found by the contentfinders, so not the 404 page, or it's an internal redirect and the web routing setting has InternalRedirectPreservesTemplate.

    If no alternative template is found the router will get the template with the file service, using the ID specified on the published content, and then assign the template to the request.

    If an alternative template is specified, the router will check if it's an allowed template for the content, if the template is not allowed on that specific piece of content it will revert to using the default template. If the template is allowed it will then use the file service to get the specified alternative template and assign the template to the request.

    Redirects

    The router will pick up the redirect and redirect. There is no need to write your own redirects:

    Missing template?

    In case the router can't find a template, it will try and verify if there's route hijacking in place, if there is, it will run the hijacked route. If route hijacking is not in place, the router will set the content to null, and run through the routing of the request again, in order for the last chance finder to find a 404.

     async ValueTask<RouteValueDictionary> TransformAsync(…)

    Only if it doesn't exist, allowing you to handle it in a custom way with a custom router handler.

  • Find the template.

  • Set the culture (again, in case it was changed).

  • Publish RoutingRequestNotification.

  • Handle redirects and missing content.

  • Initialize a few internal stuff.

  • Computes domain Uri based upon the current request ("domain.com" for http://domain.com or https://domain.com).

  • Else.

    • Sets published content request’s culture by default (first language, else system).

  • IContentFinder
    public class PublishedRequestHandler : INotificationHandler<RoutingRequestNotification>
    {
        public void Handle(RoutingRequestNotification notification)
        {
            var requestBuilder = notification.RequestBuilder;
            var content = requestBuilder.PublishedContent;
            var redirect = content.Value<string>("myRedirect");
            if (!string.IsNullOrWhiteSpace(redirect))
            {
                requestBuilder.SetRedirect(redirect);
            }
        }
    }

    Outbound request pipeline

    How the Umbraco outbound request pipeline works

    The outbound pipeline consists out of the following steps:

    1. Create segments

    2. Create paths

    3. Create urls

    To explain things we will use the following content tree:

    1. Create segments

    When the URL is constructed, Umbraco will convert every node in the tree into a segment. Each published item has a corresponding URL segment.

    In our example "Our Products" will become "our-products" and "Swibble" will become "swibble".

    The segments are created by the "Url Segment provider"

    Url Segment Provider

    The DI container of an Umbraco implementation contains a collection of UrlSegmentProviders. This collection is populated during Umbraco boot up. Umbraco ships with a 'DefaultUrlSegmentProvider' - but custom implementations can be added to the collection.

    When the GetUrlSegment extension method is called for a content item + culture combination, each registered IUrlSegmentProvider in the collection is executed in 'collection order'. This continues until a particular UrlSegmentProvider returns a segment value for the content, and no further UrlSegmentProviders in the collection will be executed. If no segment is returned by any provider in the collection a DefaultUrlSegmentProvider will be used to create a segment. This ensures that a segment is always created, like when a default provider is removed from a collection without a new one being added.

    To create a new Url Segment Provider, implement the following interface:

    Note each 'culture' variation can have a different Url Segment!

    The returned string will be the Url Segment for this node. Any string value can be returned here but it cannot contain the URL segment separator character /. This would create additional "segments" - something like 5678/swibble is not allowed.

    Example

    For the segment of a 'product page', add its unique SKU / product ref to the existing Url segment.

    The returned string becomes the native Url segment - there is no need for any Url rewriting.

    For our "swibble" product in our example content tree the ProductPageUrlSegmentProvider, would return a segment swibble--123xyz. In this case, 123xyz is the unique product sku/reference for the swibble product.

    Register the custom UrlSegmentProvider with Umbraco, either using a composer or an extension method on the IUmbracoBuilder:

    The Default Url Segment Provider

    The Default Url Segment provider builds its segments by looking for one of the below values, checked in this order:

    1. A property with alias umbracoUrlName on the node. (this is a convention led way of giving editors control of the segment name - with variants - this can vary by culture).

    2. The 'name' of the content item e.g. content.Name.

    The Umbraco string extension ToUrlSegment() is used to produce a clean 'Url safe' segment.

    2. Create paths

    To create a path, the pipeline will use the segments of each node to produce a path.

    If we look at our example, the "swibble" node will receive the path: "/our-products/swibble". If we take the ProductPageUrlSegmentProvider from above, the path would become: "/our-products/swibble-123xyz".

    Multiple sites in a single Umbraco implementation

    But, what if there are multiple websites in a single Umbraco Implementation? in this multi-site scenario then an (internal) path to a node such as "/our-products/swibble-123xyz" could belong to any of the sites, or match multiple nodes in multiple sites. In this scenario additional sites will have their internal path prefixed by the node id of their root node. Any content node with a hostname defines a “new root” for paths.

    Node
    Segment
    Internal Path

    Paths can be cached, what comes next cannot (http vs https, current request…).

    Some further considerations when working with hostnames

    • Domain without path e.g. "www.site.com" will become "1234/path/to/page"

    • Domain with path e.g. "www.site.com/dk" will produce "1234/dk/path/to/page" as path

    • No domain specified: "/path/to/page"

    3. Creating Urls

    The Url of a node consists of a complete : the Schema, Domain name, (port) and the path.

    In our example the "swibble" node could have the following URL: "http://example.com/our-products/swibble"

    Generating this url is handled by the Url Provider. The Url Provider is called whenever a request is made in code for a Url e.g.:

    The DI container of an Umbraco implementation contains a collection of UrlProviders this collection is populated during Umbraco boot up. Umbraco ships with a DefaultUrlProvider - but custom implementations can be added to the collection. When .Url is called each IUrlProvider registered in the collection is executed in 'collection order' until a particular IUrlProvider returns a value. (and no further IUrlProviders in the collection will be executed.)

    DefaultUrlProvider

    Umbraco ships with a DefaultUrlProvider, which provides the implementation for the out-of-the-box mapping of the structure of the content tree to the URL.

    How the Default Url provider works

    • If the current domain matches the root domain of the target content.

      • Return a relative Url.

      • Else must return an absolute Url.

    • If the target content has only one root domain.

    If the URL provider encounters collisions when generating content URLs, it will always select the first available node and assign the URL to this one. The remaining nodes will be marked as colliding and will not have a URL generated. Fetching the URL of a node with a collision URL will result in an error string including the node ID (#err-1094) since this node does not currently have an active URL. This can happen if an umbracoUrlName property is being used to override the generated URL of a node, or in some cases when having multiple root nodes without hostnames assigned.

    This means publishing an unpublished node with a conflicting URL, might change the active node being rendered on that specific URL in cases where the published node should now take priority according to sort order in the tree!

    Custom Url Provider

    Create a custom Url Provider by implementing IUrlProvider interface:

    The URL returned in the 'UrlInfo' object by GetUrl can be completely custom.

    If implementing a custom Url Provider, consider the following things:

    • Cache things.

    • Be sure to know how to handle schema's (http vs https) and hostnames.

    • Inbound might require rewriting.

    If there is only a small change to the logic around Url generation, then a smart way to create a custom Url Provider is to inherit from the DefaultUrlProvider and override the GetUrl() virtual method.

    Example

    Add /fish on the end of every URL. It's important to note here that since we're changing the outbound URL, but not how we handle URLs inbound, this will break the routing. In order to make the routing work again you have to implement a custom content finder, see for more information on how to do that.

    Register the custom UrlProvider with Umbraco:

    If you want to have multiple URL providers, you can add them one after the other with multiple Insert methods. Umbraco will cycle through all the providers registered until it finds one that doesn't return null. If all custom URL providers return null it will fall back to the default URL provider. The last added with Insert is the first that will be executed.

    GetOtherUrls

    The GetOtherUrls method is only used in the Umbraco Backoffice to provide a list to editors of other Urls which also map to the node.

    For example, let's consider a convention-led umbracoUrlAlias property that enables editors to specify a comma-delimited list of alternative URLs for the node. It has a corresponding AliasUrlProvider registered in the UrlProviderCollection to display this list to the Editor in the backoffice Info Content app for a node.

    Url Provider Mode

    Specifies the type of URLs that the URL provider should produce, eg. absolute vs. relative URLs. Auto is the default

    These are the different modes:

    Default setting can be changed in the Umbraco:CMS:WebRouting section of appsettings.json:

    See for more information on routing settings.

    Site Domain Mapper

    The ISiteDomainMapper implementation is used in the IUrlProvider and filters a list of DomainAndUri to pick one that best matches the current request.

    Create a custom SiteDomainMapper by implementing ISiteDomainMapper

    The MapDomain methods will receive the Current Uri of the request, and custom logic can be implemented to decide upon the preferred domain to use for a site in the context of that request. The SiteDomainMapper's role is to get the current Uri and all eligible domains, and only return one domain which is then used by the UrlProvider to create the Url.

    Only a single ISiteDomainMapper can be registered with Umbraco.

    Register the custom ISiteDomainMapper with Umbraco using the SetSiteDomainHelper extension method

    Default SiteDomainMapper

    Umbraco ships with a default SiteDomainMapper. This has some useful functionality for grouping sets of domains together. With Umbraco Cloud, or another Umbraco development environment scenario, there maybe be multiple domains setup for a site 'live, 'staging', 'testing' or a separate domain to access the backoffice. Each domain will be setup as a 'Culture and Hostname' inside Umbraco. By default editors will see the full list of possible URLs for each of their content items on each domain, which can be confusing. If the additional URLs aren't present in Culture and Hostnames, then when testing the front-end of the site on a 'staging' URL, will result in navigation links taking you to the registered domain!

    What the editor sees without any SiteDomainMapper, visiting the backoffice URL:

    Which is 'noise' and can lead to confusion: accidentally clicking the staging url, which is likely to be served from a different environment / different database etc may display the wrong content...

    To avoid this problem, use the default SiteDomainMapper's AddSite method to group Urls together.

    Since the SiteDomainMapper is registered in the DI, we can't consume it directly from a composer, so first create a component which adds the sites in the initialize method:

    Then add the component with a composer:

    Now if an editor visits the backoffice via the staging url they will only see domains for the staging url:

    Now if an editor visits the backoffice via the backoffice url they will only see domains for the backoffice url and the production url:

    NB: it's not a 1-1 mapping, but a grouping. Multiple Urls can be added to a group. Think multilingual production and staging variations, and in the example above, if an editor logged in to the backoffice via the production url, eg umbraco-v8.localtest.me/umbraco - they would see the umbraco-v8-backoffice.localtest.me domain listed.

    Grouping the groupings - BindSites

    The SiteDomainMapper contains a 'BindSites' method that enables different site groupings to be bound together:

    Visiting the backoffice now via umbraco-v8-backoffice.localtest.me/umbraco would list all the 'backoffice' grouped domains AND all the 'staging' grouped domains.

    Another Site

    another-site

    9676/

    Their Values

    their-values

    9676/their-values

    Unless HideTopLevelNodeFromPath config is true
    , then the path becomes "/to/page"
    • Use that domain to build the absolute Url.

  • If the target content has more than one root domain.

    • Figure out which one to use.

    • To build the absolute Url.

  • Complete the absolute Url with scheme (http vs https).

    • If the domain contains a scheme use it.

    • Else use the current request’s scheme.

  • If "addTrailingSlash" is true, then add a slash.

  • Then add the virtual directory.

  • Our Values

    our-values

    /our-values

    Our Products

    our-products

    /our-products

    Swibble

    swibble-123xyz

    /our-products/swibble-123xyz

    Dibble

    dibble-456abc

    Content
    URI
    IContentFinder
    WebRouting config reference documentation
    path example
    Culture and Hostnames multiple domains
    All domains listed
    Staging domain only
    Backoffice + production domains only

    /our-products/dibble-456abc

    public interface IUrlSegmentProvider
    {
      string GetUrlSegment(IContentBase content, string? culture = null);
    }
    using Umbraco.Cms.Core.Models;
    using Umbraco.Cms.Core.Strings;
    
    namespace RoutingDocs.SegmentProviders;
    
    public class ProductPageUrlSegmentProvider : IUrlSegmentProvider
    {
        private readonly IUrlSegmentProvider _provider;
    
        public ProductPageUrlSegmentProvider(IShortStringHelper stringHelper)
        {
            _provider = new DefaultUrlSegmentProvider(stringHelper);
        }
        
        public string GetUrlSegment(IContentBase content, string? culture = null)
        {
            // Only apply this rule for product pages
            if (content.ContentType.Alias != "productPage")
            {
                return null;
            }
    
            var segment = _provider.GetUrlSegment(content, culture);
            var productSku = content.GetValue<string>("productSKU");
            return $"{segment}--{productSku}".ToLower();
        }
    }
    using Umbraco.Cms.Core.Composing;
    using Umbraco.Cms.Core.DependencyInjection;
    
    namespace RoutingDocs.SegmentProviders;
    
    public class RegisterCustomSegmentProviderComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            builder.UrlSegmentProviders().Insert<ProductPageUrlSegmentProvider>();
        }
    }
    @Model.Url
    @Umbraco.Url(1234)
    @UmbracoContext.UrlProvider.GetUrl(1234);
    // This one is initialized by default
    public class DefaultUrlProvider : IUrlProvider
    {
        public virtual UrlInfo GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
        {…}
    
        public virtual IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
        {…}
    }
    public interface IUrlProvider
    {
        UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current);
    
        IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current);
    }
    using System;
    using System.Collections.Generic;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using Umbraco.Cms.Core.Configuration.Models;
    using Umbraco.Cms.Core.Models.PublishedContent;
    using Umbraco.Cms.Core.Routing;
    using Umbraco.Cms.Core.Web;
    
    namespace RoutingDocs.UrlProviders;
    
    public class ProductPageUrlProvider : DefaultUrlProvider
    {
        public ProductPageUrlProvider(
            IOptionsMonitor<RequestHandlerSettings> requestSettings,
            ILogger<DefaultUrlProvider> logger,
            ISiteDomainMapper siteDomainMapper,
            IUmbracoContextAccessor umbracoContextAccessor,
            UriUtility uriUtility,
            ILocalizationService localizationService)
            : base(requestSettings, logger, siteDomainMapper, umbracoContextAccessor, uriUtility, localizationService)
        {
        }
    
        public override IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
        {
            // Add custom logic to return 'additional urls' - this method populates a list of additional urls for the node to display in the Umbraco backoffice
            return base.GetOtherUrls(id, current);
        }
    
        public override UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
        {
            if (content is null)
            {
                return null;
            }
            
            // Only apply this to product pages
            if (content.ContentType.Alias == "productPage")
            {
                // Get the original base url that the DefaultUrlProvider would have returned,
                // it's important to call this via the base, rather than .Url, or UrlProvider.GetUrl to avoid cyclically calling this same provider in an infinite loop!!)
                UrlInfo? defaultUrlInfo = base.GetUrl(content, mode, culture, current);
                if (defaultUrlInfo is null)
                {
                    return null;
                }
                
                if (!defaultUrlInfo.IsUrl)
                {
                    // This is a message (eg published but not visible because the parent is unpublished or similar)
                    return defaultUrlInfo;
                }
                else
                {
                    // Manipulate the url somehow in a custom fashion:
                    var originalUrl = defaultUrlInfo.Text;
                    var customUrl = $"{originalUrl}fish/";
                    return new UrlInfo(customUrl, true, defaultUrlInfo.Culture);
                }
            }
            // Otherwise return null
            return null;
        }
    }
    using Umbraco.Cms.Core.Composing;
    using Umbraco.Cms.Core.DependencyInjection;
    
    namespace RoutingDocs.UrlProviders;
    
    public class RegisterCustomUrlProviderComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            builder.UrlProviders().Insert<ProductPageUrlProvider>();
        }
    }
    public enum UrlMode
    {
      /// <summary>
      /// Indicates that the url provider should do what it has been configured to do.
      /// </summary>
      Default = 0,
    
      /// <summary>
      /// Indicates that the url provider should produce relative urls exclusively.
      /// </summary>
      Relative,
    
      /// <summary>
      /// Indicates that the url provider should produce absolute urls exclusively.
      /// </summary>
      Absolute,
    
      /// <summary>
      /// Indicates that the url provider should determine automatically whether to return relative or absolute urls.
      /// </summary>
      Auto
    }
    "Umbraco": {
      "CMS": {
        "WebRouting": {
          "UrlProviderMode": "Relative"
        }
      }
    }
    public interface ISiteDomainMapper
    {
        DomainAndUri? MapDomain(IReadOnlyCollection<DomainAndUri> domainAndUris, Uri current, string? culture, string? defaultCulture);
        IEnumerable<DomainAndUri> MapDomains(IReadOnlyCollection<DomainAndUri> domainAndUris, Uri current, bool excludeDefault, string? culture, string? defaultCulture);
    }
    using Umbraco.Cms.Core.Composing;
    using Umbraco.Cms.Core.DependencyInjection;
    using Umbraco.Extensions;
    
    namespace RoutingDocs.SiteDomainMapper;
    
    public class RegisterCustomSiteDomainMapperComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            builder.SetSiteDomainHelper<CustomSiteDomainMapper>();
        }
    }
    using Umbraco.Cms.Core.Composing;
    using Umbraco.Cms.Core.Routing;
    
    namespace RoutingDocs.SiteDomainMapping;
    
    public class SiteDomainMapperComponent : IComponent
    {
        private readonly SiteDomainMapper? _siteDomainMapper;
    
        public SiteDomainMapperComponent(ISiteDomainMapper siteDomainMapper)
        {
            // SiteDomainMapper can be overwritten, so ensure it's the default one which contains the AddSite
            if (siteDomainMapper is SiteDomainMapper concreteSiteDomainMapper)
            {
                _siteDomainMapper = concreteSiteDomainMapper;
            }
        }
    
        public void Initialize()
        {
            _siteDomainMapper?.AddSite("backoffice", "umbraco-v8-backoffice.localtest.me", "umbraco-v8.localtest.me");
            _siteDomainMapper?.AddSite("preproduction", "umbraco-v8-preprod.localtest.me");
            _siteDomainMapper?.AddSite("staging", "umbraco-v8-staging.localtest.me");
        }
    
        public void Terminate()
        { }
    }
    using Umbraco.Cms.Core.Composing;
    
    namespace RoutingDocs.SiteDomainMapping;
    
    public class AddSiteComposer : ComponentComposer<SiteDomainMapperComponent>
    { 
    }
    public void Initialize()
    {
        _siteDomainMapper?.AddSite("backoffice", "umbraco-v8-backoffice.localtest.me", "umbraco-v8.localtest.me");
        _siteDomainMapper?.AddSite("preproduction", "umbraco-v8-preprod.localtest.me");
        _siteDomainMapper?.AddSite("staging", "umbraco-v8-staging.localtest.me");
        _siteDomainMapper?.BindSites("backoffice", "staging");
    }