By default Umbraco doesn't let you have two documents with the same name underneath the same parent. I needed to change that.
If you're wondering why...
(if not, skip to the how)
In my particular case this is for news articles and event pages.
My news and event pages use a custom IUrlProvider
and IContentFinder
(as per the docs) so that my news and events pages can have URLs like this:
/events/2022/03/16/my-awesome-event/
I have multiple events with the same name, but I want to keep all of my events underneath a single parent node in the backoffice. By default, if I create two events called "My Awesome Event" beneath the same node in the backoffice I'll end up with "My Awesome Event" and "My Awesome Event (1)", like this:
The URL changes too:
/events/2022/03/16/my-awesome-event/
/events/2022/04/20/my-awesome-event-1/
I could use date "folders", and automate this by moving the articles/events on save, but this adds extra nodes and hierarchy that I don't want or need. Having them all in one list view will make for a much nicer editor experience (in this site).
A bit of googling reminded me that in v7 we had the option to set ensureUniqueNaming
to false in the config. That's not available in v8, and was a bit of a blunt instrument anyway.
Turns out that in v9 (and probably in v8 too, but I haven't checked) there's a smarter way...
Here's how
EnsureUniqueNaming
is still a property on the DocumentRepository
and it's used to determine if the repository allows non-unique names. It's set to false, and I thought about inheriting this class and simply setting this to true - but that would set it to true for all content, and that seems risky. I have routing in place to make sure that non-unique names for my news/events don't matter, but if when an editor creates a page with a duplicate name elsewhere it just wont work.
Fortunately, there's an overridable method that means we can be a bit smarter - EnsureUniqueNodeName
.
In this method I have access to the parent id, so I can check where I'm saving content and conditionally decide to allow duplicate node names or not.
Here's what I ended up doing:
public class CustomDocumentRepository : DocumentRepository
{
private static readonly string[] allowNonUniqueChildNames = { EventsPage.ModelTypeAlias, NewsPage.ModelTypeAlias };
// constructor ommited for brevity
protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0)
{
var parent = this.Get(parentId);
if (allowNonUniqueChildNames.InvariantContains(parent.ContentType.Alias))
{
return nodeName;
}
return base.EnsureUniqueNodeName(parentId, nodeName, id);
}
}
In this case the content type aliases are taken from my ModelsBuilder models.
Thanks to Dependency Injection, it's super easy to replace the existing repository with our custom one. Here's an example using startup.cs, make sure to either add the service after the AddUmbraco
extension or add it to your own builder extension method which is what I did.
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.Build();
.Build();
services.AddUnique<IDocumentRepository, CustomDocumentRepository>();
}
This could be better...
This approach only really works where it makes sense to scope non-unique names based on the parent node - though that doesn't have to be based on type, it could be a property value etc. In my case, an "events" node only has child events and a "news" node only child news articles, so this is fine.
Ideally, I'd want to take more things into account such as the type or even content of the node that I am saving, but this method doesn't know anything about the content other than the name. I could override other methods but that would result in a lot of code duplication (just look at all those private methods). Perhaps there's scope for adding a notification here to allow control over how and when unique names are applied, or at least overloading the method to take the content itself into account. If I get round to making a PR for that I'll update this post.
Top comments (1)
This worked very nicely for me until I tried to use it on a document type with "Allow vary by culture" enabled