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.
-
- The root node holds the main domain.
-
- The home node hold the website as the subdirectory of the domain
-
- Other nodes under the root could be subdomains.
-
- RemoveHomeUrlProvider class removes the home from the url
-
- 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!