Resolving Content Links with taxonomy or sitemap location

mmandersonmmanderson Member
edited February 7 in Back-end Development

I have actions that retrieve content based on taxonomy or sitemap location. It would be super useful if I could resolve content link urls in my CustomContentLinkUrlResolver using the same information. Unfortunately, the ContentLink doesn't contain this.

The Sample App uses ContentTypeCodename to resolve links, which is probably very useful if a site hierarchy happens to match the content types. In my experience content urls end up changing, especially on marketing sites, without the content changing. So I've avoided basing urls on the content type.

I've read the article for Managing Navigation with a new Navigation Item content type, but I'd rather not have to create a navigation item for each page on my site. If I can retrieve content based on filters, then it would make more sense to resolve content links the same way in my CustomContentLinkUrlResolver. I just haven't figured out how without modifying the SDK.

Comments

  • JanL@kentico.comJanL@kentico.com Czech RepublicMember, Administrator, Kentico Staff admin
    edited February 8

    Hello @MichelleAnderson ,

    Thanks for the tip! The sitemap feature was designed in the older days, purely for content editors. We plan on redesigning it in the future. But, I think you can create a logic very similar to the one in my above mentioned article. The difference would be that instead of a hierarchy of Navigation Item content items, you'd capture the hierarchy in a dedicated taxonomy group. In it, you'd just create taxonomy terms with the middle-of-the-path URL slugs as their values. The end-of-the-path URL slugs would be stored in the content items, as usual. The NavigationProvider class would work with that taxonomy group instead of the navigation items.

    Rendering and resolving of the links would require some changes in the NavigationProvider and ContentResolver classes. As each links object contains just the id, codename, type and url_slug fields, resolving of the links would require you to have a method in the NavigationProvider class that would

    1. fetch lightweight projections of all content items (i.e. just their system object and the title) by their current location in the navigation taxonomy group,
    2. store the hierarchy in the cache,
    3. eventually re-create the cache upon webhook calls notifying changes of either the taxonomy group or the URL slugs of content items.

    Now, the ContentResolver should be able to resolve menu item clicks using your taxonomy hierarchy. It would

    1. take the URL path obtained from the browser,
    2. traverse down the cached hierarchy and seek for matching taxonomy terms, until it finds the right content item URL slug element,
    3. request the content item(s) by its codename and return it to the browser.

    Your IContentLinkResolver implementation could then work similarly to the MenuItemGenerator class. It would:

    1. take the ContentLink.Id as input,
    2. traverse down the cached hierarchy and seek for matching taxonomy terms, until it finds the right content item GUID (corresponding to the ContentLink.Id),
    3. return the concatenated taxonomy URL slugs, together with the content item's URL slug, as the resolved URL.

    Did this help? I'll be happy to dig more into that, should you wish.

  • mmandersonmmanderson Member

    Thanks @JanL@kentico.com ! I created my site hierarchy with a navigation taxonomy group. I added it to my Metadata snippet so I can also use it to create the canonical tag. I see now that using the sitemap location isn't a good idea because it can be too easily changed. The taxonomy solution works better.

    The only bit I have to figure out is how to get it into my CustomContentLinkUrlResolver. I've run into a circular dependency issue with the NavigationProvider and the DeliveryClient. But I'm a lot closer than I was before.

  • JanL@kentico.comJanL@kentico.com Czech RepublicMember, Administrator, Kentico Staff admin

    Good point with the Metadata snippet, @mmanderson !

    In order to help you out with that circular dependency, I'd probably need to see the specific source code. I've also corrected my yesterday's answer because I've realized that the link resolver should not interact with the content resolver. Instead, the link resolver should work in a similar way the MenuItemGenerator class works.

  • mmandersonmmanderson Member

    I'm working on this bit. I need to get the NavigationProvider into ContentLinkUrlResolver but the NavigationProvider depends on the DeliveryClient.

    services.AddSingleton<IDeliveryClient>(c => new CachedDeliveryClient(c.GetRequiredService<IOptions<ProjectOptions>>(), c.GetRequiredService<IMemoryCache>())
      {
         CodeFirstModelProvider = { TypeProvider = new CustomTypeProvider() },
        ContentLinkUrlResolver = new CustomContentLinkUrlResolver()
       });
    
     services.AddSingleton<INavigationProvider>(c => new NavigationProvider(c.GetRequiredService<IOptions<NavigationOptions>>(), c.GetRequiredService<IDeliveryClient>(), c.GetRequiredService<IMemoryCache>()));
    
  • mmandersonmmanderson Member

    Since the methods in IContentLinkUrlResolver in the SDK aren't async, I don't think I can do it this way. Unless I'm missing something...

  • JanL@kentico.comJanL@kentico.com Czech RepublicMember, Administrator, Kentico Staff admin

    Hi @mmanderson ,

    I got your point. I'm afraid we will have to lower our expectations of the DI logic. The first solution that came to my mind was to instantiate the delivery client in the link resolver without DI:

    public class CustomContentLinkUrlResolver : IContentLinkUrlResolver
    {
        private readonly INavigationProvider _navigationProvider;
        private readonly IMemoryCache _memoryCache;
    
        public CustomContentLinkUrlResolver(IOptions<ProjectOptions> projectOptions, IOptions<NavigationOptions> navigationOptions, IMemoryCache memoryCache)
        {
            // null checks
    
            _navigationProvider = new NavigationProvider(navigationOptions, new DeliveryClient(projectOptions.Value.KenticoCloudProjectId), memoryCache);
            _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
        }
    

    Then, your Startup code would look like:

    var serviceProvider = services.BuildServiceProvider();
    var contentLinkUrlResolver = new CustomContentLinkUrlResolver(serviceProvider.GetRequiredService<IOptions<ProjectOptions>>(), serviceProvider.GetRequiredService<IOptions<NavigationOptions>>(), serviceProvider.GetRequiredService<IMemoryCache>());
    
    services.AddSingleton<IDeliveryClient>(c => new CachedDeliveryClient(c.GetRequiredService<IOptions<ProjectOptions>>(), c.GetRequiredService<IMemoryCache>())
    {
        CodeFirstModelProvider = { TypeProvider = new CustomTypeProvider() },
        ContentLinkUrlResolver = contentLinkUrlResolver
    });
    

    But, it might be possible to use Lazy<T>. I think it is worth experimenting with that.

    BTW, the lack of async in the signature of the link provider's methods could be easily mitigated by using Task.Run():

    var navigation = Task.Run(() => _navigationProvider.GetNavigationAsync()).Result;
    

    Hope it helps.

Sign In or Register to comment.