Remove the "home" segment from URLs

In a Multisite scenario, especially in localhost development I am having a bit of a challenge in getting the url rewriting to work appropriately.

Goal: customize how URLs are generated for Umbraco content nodes. Specifically, to remove the “home” segment from URLs so that the root node (often named “home” in Umbraco) does not appear in the public URLs of the site.

Multi site configuration:

  - Home
    - About

- RootSiteB (root node for Site B)
  - Home
    - Blog

How I’m attempting to accomplish this.

    1. The root node holds the main domain.
    1. The home node hold the website as the subdirectory of the domain
    1. Other nodes under the root could be subdomains.
    1. RemoveHomeUrlProvider class removes the home from the url
    1. HomePathContentFinder places the “home” back into the url for content lookup in Umbraco engine.

Code classes to follow - this is not working… the home is removed but the content cannot be found…

What is happening is a url like localhost:xxx/domain/home/about is not found in a multi site environment. I am unable to do any testing. It keeps defaulting to the first root node.

RemoveHomeUrlProvider:

using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using static Umbraco.Cms.Core.Constants.Conventions;

public class RemoveHomeUrlProvider : IUrlProvider
{
    public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri? current)
    {
        if (content == null || current == null)
            return null;

        // Find the root node (the one with no parent)
        var rootNode = content.AncestorsOrSelf().FirstOrDefault(x => x.Parent == null);

        // Build segments, skipping the root node
        var segments = new List<string>();
        var currentNode = content;
        while (currentNode != null && currentNode.Id != rootNode.Id)
        {
            segments.Insert(0, currentNode.UrlSegment);
            currentNode = currentNode.Parent;
        }

        // Remove the first occurrence of "home" (case-insensitive)
        var homeIndex = segments.FindIndex(s => s.Equals("home", StringComparison.OrdinalIgnoreCase));
        if (homeIndex >= 0)
            segments.RemoveAt(homeIndex);

        var newPath = "/" + string.Join("/", segments);

        // Use only the scheme, host, and port from 'current'
        //var baseUri = new UriBuilder(current.Scheme, current.Host, current.Port).Uri;

        // Combine with the correct domain (no root node segment in path)
        string baseUrl = current.ToString();
        if (!baseUrl.EndsWith("/"))
            baseUrl += "/";

        string segment = newPath.TrimStart('/');

        var fullUrl = new Uri(baseUrl + segment);

        return new UrlInfo(fullUrl.ToString(), true, culture);
    }


    public IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
    {
        return Enumerable.Empty<UrlInfo>();
    }
}

and HomePathContentFinder (puts the home back into url before content lookup):

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;

public class HomePathContentFinder : IContentFinder
{
    // TODO: uncomment the composer to allow this service to run.
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<HomePathContentFinder> _logger;

    public HomePathContentFinder(IServiceProvider serviceProvider, ILogger<HomePathContentFinder> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
public Task<bool> TryFindContent(IPublishedRequestBuilder request)
{
    // Log the request URI being processed
    _logger.LogInformation("Finder triggered for URI: {Uri}", request.Uri);

    // Create a DI scope to resolve scoped services
    using var scope = _serviceProvider.CreateScope();
    var contentQuery = scope.ServiceProvider.GetRequiredService<IPublishedContentQuery>();

    // Get the domain (which contains the hostname and root node ID)
    var domain = request.Domain;

    // If no domain is assigned, exit early
    if (domain == null)
        return Task.FromResult(false);

    // Get the root node ID assigned to the domain
    var rootNodeId = domain.ContentId;

    // Get the actual root node using the published content query
    var rootNode = contentQuery.Content(rootNodeId);
    if (rootNode == null)
    {
        _logger.LogWarning("No root node found.");
        return Task.FromResult(false);
    }

    // ✅ Find the "home" node under root by document type alias instead of name
    var homeNode = rootNode.Children()
        .FirstOrDefault(x => x.ContentType.Alias.Equals("home", StringComparison.OrdinalIgnoreCase));

    // If "home" node isn't found, log and exit
    if (homeNode == null)
    {
        _logger.LogWarning("No node with alias 'home' found under root.");
        return Task.FromResult(false);
    }

    // Get the original path from the incoming URL (e.g., "/about-us")
    var originalPath = request.Uri.AbsolutePath;

    // Prepend the "home" node's path to simulate routing under /home
    var newPath = homeNode.Url().TrimEnd('/') + (originalPath == "/" ? "" : originalPath);

    // Normalize the new path for comparison
    var normalizedPath = newPath.TrimEnd('/').ToLowerInvariant();

    // Log the resolved virtual path we’ll search for in Umbraco content
    _logger.LogInformation("Looking for content at: {Path}", normalizedPath);

    // Search the home node and its descendants for a node with a matching URL
    var content = homeNode
        .DescendantsOrSelf()
        .FirstOrDefault(x =>
            x.Url().TrimEnd('/').Equals(normalizedPath, StringComparison.OrdinalIgnoreCase));

    // If no matching content is found, log and exit
    if (content == null)
    {
        _logger.LogWarning("No content found at: {Path}", normalizedPath);
        return Task.FromResult(false);
    }

    // Log success and assign the resolved content to the request
    _logger.LogInformation("Content found: {Name} ({Id})", content.Name, content.Id);
    request.SetPublishedContent(content);

    // Indicate the request has been handled successfully
    return Task.FromResult(true);
}


}

Has anyone attempted this before? Is there something I need to do differently for localhost opposed to each root node having a normalized domain?

Double high five for any solution!

Hmm I could be completely misunderstanding your issue but I believe you might have the wrong structure in your content.

What I’d do is have a root for each site, set culture and hostname for each one (if they have different domains)

- Root A (root node for Site A) 
  - Blog
  - otherPage

- Root B (root node for Site B)
  - Blog
  - otherPage

If, by any reason, you don’t want Root A to be present in the url, I would set, in appSettings.json, Umbraco:CMS:Global:HideTopLevelNodeFromPath to true.

2 Likes

@PMendesWebDev is right. Typically the root node is the home node. The simple fix is to split them into sibling nodes as stated, and bind each root node to the desired domain.

- SiteX.com (hostname bound)
  - page1

- SiteX.subdomain.com (hostname bound)
  - page1

- SiteY.com (hostname bound)
  - page1

however, Umbraco does support setting a culture/hostname binding on any node, so you could actually create a child node of one site and bind a different hostname to it, so that should work as well. Keep in mind though that the node with a hostname bound is the home node of that domain. The below should be possible but be careful you don’t make it too messy / complicated. Simple is usually best.

- SiteX.com (SiteX.com hostname bound)
  - page1
  - page2 (SiteX.subdomain.com hostname bound - new home node of SiteX.subdomain.com)
    - page 3 (SiteX.subdomain.com/page-3/)
- SiteY.com (hostname bound)
  - page1

If you are wanting to store sitewide values in a node, I typically create a “Configuration” node (without a template) under the desired home node. You can then restrict any content on / under the “Configuration” node from rendering on the front end as needed.

- SiteX.com (hostname bound)
  - Configuration
  - page1
  - page2

Thank you @PMendesWebDev and @asawyer for your kind response and attempt to help.

Firstly, this new forum platform is not ideal. I was not able to edit the initial message after submission.

The correct site architectures is:

- RootSiteA (root node for Site A - settings specific to this company)
  - Home
    - Blog
  - InternalJobsSystem (Handeled by culture and hostname with back-end access settings)
    - ListingsPage


- RootSiteB (root node for Site B - settings specific to this company)
  - Home
    - About
  - Subdomain
    - SingleSign0nPage
  - Country1Site
    - Page1
    - Page2
    - EmployeePortal
      - Info
      - Company1
        - Info
      - Company2
        - Info
  - Country2Site
    - Page1
    - EmployeePortal
      - Info
      - Company1
        - Info
      - Company2
        - Info

There is a reason for this. This is a fortune 500 company with an Enterprise Solution that requires multi sites with reusable components, content, subdomains, internal software, external software, etc. There are multiple brands under one umbrella. This proposed architecture is elegant and just the tip of the iceberg for the requirements.

Your suggestions are a good work-around to what I am attempting to accomplish (removing “home” from the url) but will make the back-office hard to manage with all of the users allowed only into specific groups, portals, sub-portals, etc. etc.

I really need help with the removal of the home node in the url - hence my almost, but not quite working, code.

Yes, each individual RootNode handles the “settings” of that company and each company can have multiple websites.

This already done. Good suggestion. Also have a redirect to the home node.

This is precisely what I am trying to do on grander scale. The problem is now your logic to separate the nodes for navigation and content sharing becomes more complicated to manage than just removing the “home” segment from the visible url.

Hey

This package offers this feature by doc type alias.

Hope this helps

Matt

1 Like

I’d try the package first, that’s probably the easiest.

This is how umbraco builds urls:

You can take complete control of generated urls by implementing your own IUrlProvider and IContentFinder. The url provider makes the urls, contentfinder takes a request and locates an umbraco node. If going this route, try to use caching where possible, as you want to streamline umbraco content lookups.

Here is an overall walkthrough: