DEV Community

Cover image for Creating Duplicate Content in Umbraco without Duplicating Content
Søren Kottal
Søren Kottal

Posted on

Creating Duplicate Content in Umbraco without Duplicating Content

From time to time, I run into projects where I need an exact copy of a specific page in another part of the content tree in Umbraco.

The simple and easy way to handle this out of the box is to copy the page to where you need it, but then you have to maintain the same page in multiple places.

Can it be done in a better way?

Of course, pretty much anything in Umbraco can be done the way you like it. So here's how I did it...

The basics of this are a Content Type called Copy Of Node. In this, I add a content picker where the editor can pick the page to copy.

Screenshot of the Content Type editor in Umbraco editing the Content Type named Copy Of Node, containing a content picker, where the editor can pick the page to copy.

For the sake of example, I have added this to Paul Seals wonderful Clean Starter Kit and created a duplicate of the blog article about popular blogs.

Screenshot of the content editor in Umbraco edited the node of type Copy of a node, referencing a blog post

Going to the website, we can see that our new copy of the node is exactly the same as the original. Happy days!

Screenshot of the Hello node, that copies the content from the Popular blogs node

Screenshot of the Popular blogs node

And in the source of our copy, we can see the canonical tag pointing to the original content, eliminating any SEO concerns for duplicate content.

Screenshot of the source code of the Hello node, showing the canonical link pointing the the Popular blogs node

How does it work?

This all works by fiddling with the routing in Umbraco. I have created a notification handler, listening to a RoutingRequestNotification. You can see below, how I've done.

Remember to add the notification handler, either in your Startup.cs, or by using a composer.

using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Web.Common.PublishedModels;

namespace Umb11Clean;

public class PublishedRequestHandler : INotificationHandler<RoutingRequestNotification>
{
    private readonly IRequestCache _requestCache;

    public PublishedRequestHandler(AppCaches appCaches)
    {
        _requestCache = appCaches.RequestCache;
    }
    public void Handle(RoutingRequestNotification notification)
    {
        var requestBuilder = notification.RequestBuilder;
        var content = requestBuilder.PublishedContent;

        if (content is CopyOfNode copyOfNode && copyOfNode.CopiedContent is not null)
        {
            // redirect internally to the copied content, and set the right template
            notification.RequestBuilder.SetInternalRedirect(copyOfNode.CopiedContent);
            notification.RequestBuilder.TrySetTemplate(copyOfNode.CopiedContent.GetTemplateAlias());

            // Add the actual content to the request cache for later retrieval
            _requestCache.Set("ActualContent", content);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the notification handler, you can see that I fetch the published content from the request, and check if the content is of the content type CopyOfNode and there is copied content.

If there is, I set an internal redirect to the copied content, and set the template to the copied contents template.

Note, an internal redirect in Umbraco is more like a rewrite, so the url doesn't change - but all the content does.

All this will make Umbraco think that we are actually on the copied node, making eg. tree crawling from the copy impossible. As an example, if you generate navigations based on the current node being viewed, you don't want to generate it based on the copied node, but the copy of the copied node - the actual node.

Because of this, I add the actual content - the copy - to the request cache for later retrieval.

Treecrawling and working of the node context

To use the actual content as a base for crawling the content tree, or do other stuff, I add a ContentHelper, that can fetch me the actual current content. (Also, remember to register this, so you can inject it in later)

It looks like this:

using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Web;

namespace Umb11Clean.Helpers;

public class ContentHelper
{
    private readonly IRequestCache _requestCache;
    private readonly IUmbracoContextAccessor _umbracoContextAccessor;

    public ContentHelper(
        AppCaches appCaches,
        IUmbracoContextAccessor umbracoContextAccessor)
    {
        _requestCache = appCaches.RequestCache;
        _umbracoContextAccessor = umbracoContextAccessor;
    }

    public IPublishedContent? GetActualContent()
    {
        var actualContent = _requestCache.GetCacheItem<IPublishedContent>("ActualContent");

        if (actualContent is not null) return actualContent;

        return _umbracoContextAccessor.GetRequiredUmbracoContext().PublishedRequest?.PublishedContent;
    }
}
Enter fullscreen mode Exit fullscreen mode

The ContentHelpers GetActualContent method fetches the actual content from the request cache, and returns that. If there is no actual content in the request cache, it simply falls back to the published content from the Umbraco context.

So wherever you are getting the content using the PublishedRequest on the UmbracoContext, change this to use the ContentHelper instead.

With this helper in place, we can now generate eg. navigations based on where we actually are in the tree - the copy of a node in stead of the copied node.

In Clean Starter Kit, I found this snippet for generating navigation, where a link is underlined, if it is an ancestor (or self) of the current page.

@foreach (var page in homePage.Children.Where(x => !x.Value<bool>("umbracoNaviHide")))
{
    <li class="nav-item">
        <a 
            class="nav-link px-lg-3 py-3 py-lg-4 @(page.IsAncestorOrSelf(Model) ? "text-decoration-underline" : "")" 
            href="@(page.Url())">
            @(page.Name)
        </a>
    </li>
}
Enter fullscreen mode Exit fullscreen mode

This will make it underline the path where the node is copied from - alas the copied node. In stead we want it to underline where the node is copied to - alas the copy. You can see it on the screenshot below, where Blogs are underlined, instead of Hello.

Screenshot of navigation underlining Blogs

To fix this, inject the ContentHelper into the view, and fetch the actual content node.

@inject Umb11Clean.Helpers.ContentHelper _contentHelper
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels
@{
    var homepage = Model.AncestorOrSelf<ContentModels.Home>();
    var actualContent = _contentHelper.GetActualContent() ?? Model;
}

// ... removed for brevity

@foreach (var page in homePage.Children.Where(x => !x.Value<bool>("umbracoNaviHide")))
{
    <li class="nav-item">
        <a 
            class="nav-link px-lg-3 py-3 py-lg-4 @(page.IsAncestorOrSelf(actualContent) ? "text-decoration-underline" : "")" 
            href="@(page.Url())">
            @(page.Name)
        </a>
    </li>
}
Enter fullscreen mode Exit fullscreen mode

Now we determine the underlining by checking on the actual content, and the result is as expected.

Screenshot of navigation underlining Hello

So with this, we can now copy content to all kinds of places in the content tree and only maintain them from their origin, while keeping SEO friendlyness making sure that all the copies points their canonical links back to the original.

Top comments (1)

Collapse
 
stef111 profile image
Stefanie Mills

This is the article I was looking for, so thank you for helping. Could you please tell me what software you use to run your incredibly fast website? I also want to create a simple website for my business, but I need help with the domain and hosting. Asphostportal reportedly has a stellar reputation. Are there any other choices available, and if so, what would you suggest?