How to Remove the Url Segment of Cotanier Pages?

Vote:
 

Hello everyone!

My site uses several container pages (a type that inherits from PageData without a view/controller) throughout the content tree so I can organize the pages. Of course, the container page URL segment appears in the final URL of the pages. I would like to include a checkbox in the Container Page to show/hide this URL segment. How to achieve this?

Thanks!

#334540
Dec 19, 2024 5:28
Vote:
 

Hi Kevin,

This may sound obvious but I think it's worth a warning before continuing - the code for URL generation and resolution is called pretty much constantly when rendering content so, if you're going to override it, make sure your changes are efficient. I've seen it done badly before and it can grind a site to a halt.

With the warning out of the way, the simplest way I've found to modify page URLs in this way is to use the "Simple Address" field (assuming it's not already in use). This takes care of a number of the complexities you would otherwise face, notably, the simple address field is already set up to route to the content so you don't need to write your own routing. It'll also warn you about URL clashes which are a very real possibility when you start removing segments from URLs.

Expecting users to manually enter a URL minus the container pages is clearly not a great plan so you can automatically set the simple address by hooking in to the SavingContent event and traversing the page's ancestors (minus the containers) to work out what the new path should be.

You'll also want to ensure that, whenever the content is linked, the simple address is used rather than the original URL. You can do this by hooking into the GeneratedUrl event and overriding the URL.

Putting it all together, a simple solution might look like this:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class CustomRoutingInitialisation : IInitializableModule
{
    private IContentEvents _contentEvents;
    private IContentLoader _contentLoader;
    private IContentUrlGeneratorEvents _contentUrlGeneratorEvents;

    public void Initialize(InitializationEngine context)
    {
        _contentEvents = context.Locate.ContentEvents();
        _contentLoader = context.Locate.ContentLoader();
        _contentUrlGeneratorEvents = context.Locate.Advanced.GetInstance<IContentUrlGeneratorEvents>();

        //Handle the appropriate events
        _contentEvents.SavingContent += HandleSavingContentEvent;
        _contentUrlGeneratorEvents.GeneratedUrl += HandleGeneratedUrlEvent;
    }

    private void HandleGeneratedUrlEvent(object sender, UrlGeneratorEventArgs e)
    {
        //Don't rewrite the URL in edit/preview mode
        if (e.Context.ContextMode != ContextMode.Default)
        {
            return;
        }

        //Use the simple address (ExternalURL) value as the URL for this content if it exists
        if (_contentLoader.Get<IContent>(e.Context.ContentLink) is PageData page && !string.IsNullOrEmpty(page.ExternalURL))
        {
            e.Context.Url.Uri = new Uri($"{page.ExternalURL}/", UriKind.Relative);
        }
    }

    private void HandleSavingContentEvent(object sender, ContentEventArgs e)
    {
        //Set the simple address (ExternalURL) on save, assuming it's not already been set
        if (e.Content is PageData page && string.IsNullOrEmpty(page.ExternalURL))
        {
            page.ExternalURL = GetShortenedUrl(page);
        }
    }

    private string GetShortenedUrl(PageData page)
    {
        //Get ancestors excluding any container pages (and the home page - we don't need that segment)
        var ancestors = _contentLoader.GetAncestors(page.ContentLink).Where(x => x is PageData && x is not StartPage && x is not ContainerPage);

        //Get the URL segments for each ancestor, starting from the furthest from the page and combine them into a URL
        var ancestorPath = string.Join("/", ancestors.Reverse().Select(x => (x as PageData).URLSegment));
        return $"{ancestorPath}/{page.URLSegment}";
    }

    public void Uninitialize(InitializationEngine context)
    {
        _contentEvents.SavingContent -= SavingContent;
        _contentUrlGeneratorEvents.GeneratedUrl -= GeneratedUrl;
    }
}

One point worth noting is that, using this approach in its current form, the URL wouldn't automatically change if it were moved elsewhere in the site structure or an ancestor page changed its URL so you'd need to handle that scenario. It also excludes all container pages (for the sake of keeping the code simple) so, if you want to make it an option on the container page whether to exclude it from the URL, you'll need to modify the GetShortenedUrl function to take that into account. You'd also need to regenerate any URLs of descendant pages when a container switches between visible in URLs and not.

#334561
Dec 19, 2024 18:37
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.