Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Juan C Rois 6 posts 99 karma points
    Jul 06, 2020 @ 17:36
    Juan C Rois
    0

    Virtual Content and Custom URL

    Hello everybody!

    First of all, Thanks to all the Umbraco developers out there. I've been using Umbraco for 18 months and the questions/answers in this forum have helped me a lot.

    I need help with something that I'm very confused about. Thanks in advance for any feedback I get.

    I want to be able to render published content nodes (as virtual nodes) under a single node/template.

    I have 2 root level content nodes (created from their respective document types):
    Home
    Catalog

    Under "Home" I have landing pages and child pages:
    Home
      ↳ Products (landing page)
        ↳ Category (child page)
      ↳ Locations (landing page)
        ↳ Location (child page)
      ↳ ...

    Under "Catalog" I have a similar structure:
    Catalog
      ↳ Products
        ↳ Categogy
          ↳ Product 1
          ↳ Product 2

    What I'm trying to accomplish is to be able to serve "Product 1" on "mywebsite.com/products/category/product-1".
    As is, "mywebsite.com/products/category/product-1" would not work given that there is no content node or template associated with "product 1" under "/products/category".

    I was able to get it to work using an instance of "IContentFinder". The problem with that approach is that even though I got the content node displayed in the page. All the document type properties of the ancestors were missing.

    Furthermore, I used:

    var path = contentRequest.Uri.GetAbsolutePathDecoded();
    if (path.StartsWith("/products/category"))
    {...}
    

    That ended up loading the content under "mywebsite.com/products/category", replacing the content node that belongs to the "category (child page)". Which is not virtual.

    Besides the Pipeline/IContentFinder, I've looked into "Custom Routes" and "Custom Controllers (highjacking)" and I'm confused about which approach is the one I need.

    In the end, I need to implement a process where I serve a published content node into the 3rd (virtual) nested level of the content tree, while also having the model(s) of the ancestors (2nd and 1st level).
    All by accessing an URL in which the 3rd segment is dynamic, as in:
    "mywebsite.com/parentContentNode/childContentNode/anyVirtualNode"

    My apologies for not asking a direct question. I just really can't think of the right question to ask.

    Thank you.

  • Amir Khan 1287 posts 2744 karma points
    Jul 06, 2020 @ 21:13
    Amir Khan
    1

    Have you thought about using query strings to pass the ID of the content into a view and then using rewrite rules to mask the URL? Its not trivial, but not crazy complicated either.

  • Juan C Rois 6 posts 99 karma points
    Jul 06, 2020 @ 21:35
    Juan C Rois
    0

    Thanks for your feedback Amir. I've had always considered rewrite rules a "last resort". I'm not opposed to use that approach. I just want to explore the tools that Umbraco offers.

  • Amir Khan 1287 posts 2744 karma points
    Jul 06, 2020 @ 21:36
    Amir Khan
    0

    They're annoying for sure but consistent.

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    Jul 07, 2020 @ 07:48
    Marc Goodson
    1

    Hi Juan

    I think you are on the right track with the custom IContentFinder!

    But what you don't want to do is return 'AS IS' the content for the product, in the location you've found it in.

    What I think you need to do based on your description above, is in your IContentFinder - create a new object that implements IPublishedContent based upon your found product - and then set it's 'Parent' to be the place in the tree where it needs to 'appear' to be found - that way breadcrumbs / ancestor 'lookups' will work as 'virtually' expected.

    There is a handy helper class called PublishedContentWrapped that allows you to construct such an object from another IPublishedContent object. and then override the properties that you need to 'alter'

    eg something like this:

     public class VirtualProductPublishedContent : PublishedContentWrapped
        {
            private readonly IPublishedContent _productPublishedContent;
            private readonly IPublishedContent _categoryPublishedContent;
    
            public VirtualProductPublishedContent(IPublishedContent productPublishedContent, IPublishedContent categoryPublishedContent) : base(productPropertyContent)
            {
                _productPublishedContent = productPublishedContent;
                _categoryPublishedContent = categoryPublishedContent;
            }
    
            /// <summary>
            /// properties can be overriden with our own implementation - here we set the 'parent' item for the virtual property to be the published Umbraco Category Page
            /// </summary>
            public override IPublishedContent Parent
            {
                get { return _categoryPublishedContent; }
    
            }
            public override string Path
            {
                get
                {
                    return _categoryPublishedContent.Path + "," + _productPublishedContent.Id;
                }
            }
    

    or at least that's the gist of it, your IContentFinder would return this hybrid object, instead of the Product IPublishedContent item.

    regards

    Marc

  • Juan C Rois 6 posts 99 karma points
    Jul 07, 2020 @ 13:12
    Juan C Rois
    0

    Thank you very much Marc.
    I saw the the part about the "publishedContentWrapped" in the "IContentFinder" documentation page . But I found it somewhat vague for my level of understanding.
    Your code example above fills in one of the blanks.
    Thank you so very much!

    Since I have your attention, may I ask a follow up?
    I still need/want the 3rd segment in my URL to be dynamic. The 3rd segment is what I'll use to find the published content I need.

    Do I need to implement a "Custom Route" that includes that 3rd segment? I am a little confused about that too. Even though I want the 3rd segment to be virtual, IT IS part of the url that triggers the request.

    Again, THANK YOU very much.

  • Juan C Rois 6 posts 99 karma points
    Jul 08, 2020 @ 00:50
    Juan C Rois
    101

    I solved my problem with @Marc's help.
    Here's what I did. Hopefully it will help someone else.

    Read above to understand the challenge.
    TL: DR; Use the 3rd segment in the URL as virtual/dynamic to serve any content node under a single docType-content-template.

    Step 1: Register a content finder as described in the IContentFinder documentation. I skipped the "remove, append and insert" statements.

    Step 2:
    I created a folder in the root of the project. I named it "App_Code". Inside that folder I created a c# class (.cs file) named ProductContentFinder.cs.
    "ProductContentFinder" is also what needs to go in the contentFinder composition, like so:

    composition.ContentFinders().InsertBefore<ContentFinderByUrl, ProductContentFinder>();   
    

    Step 3:

    using Examine;  
    using System.Collections.Generic;  
    using System.Linq;
    using Umbraco.Core;
    using Umbraco.Core.Models.PublishedContent;
    using Umbraco.Examine;
    using Umbraco.Web.Routing;
    
    namespace Myproject.App_Code
    {
        public class ProductContentFinder : IContentFinder
        {
            public bool TryFindContent(PublishedRequest contentRequest)
            {
                var path = contentRequest.Uri.GetAbsolutePathDecoded();
                var pathSegments = contentRequest.Uri.Segments.Length;
                if(path.Contains("/products/automotive") && pathSegments > 3) // products is my landing page. automotive is my child page and only execute if the url has 3rd segment (the virtual one)
                {
                    var virtualSegment = contentRequest.Uri.Segments.Last(); // this will used to find the desired virtual node
                    if (ExamineManager.Instance.TryGetIndex("ExternalIndex", out var index)) // decided to use Examine to search for the virtual node
                    { 
                        var productPublishedContent = new List<IPublishedContent>(); // just a container
                        var targetLandingPage = new List<IPublishedContent>(); // just a container
                        var productSearcher = index.GetSearcher(); // dedicated searcher for the product (virtual node)
                        var productResult = productSearcher.CreateQuery("content").NodeTypeAlias("productProfile").And().Field("urlName", virtualSegment).Execute(1); // run the query and limit to 1 item. "urlName will match the virtualSegment
                        if (productResult.Any())  // did we get anything?
                        { 
                            foreach(var result in productResult) // should loop only once because the query has a limit of 1
                            {
                                var productNode = contentRequest.UmbracoContext.Content.GetById(int.Parse(result.Id)); // returns the IPublishedContent node
                                productPublishedContent.Add(productNode); // add to the empty container. the container was declared outside the if and foreach for scope purposes
                                var landingPageSearcher = index.GetSearcher();  // dedicated searcher for the target landing page
                    // given the structure on the content tree matches the structure of the catalog (see above), use the name of the parent node to find the target landng page
                                var landingPageResult = landingPageSearcher.CreateQuery("content").NodeTypeAlias("childPage").And().Field("nodeName", directoryNode.Parent.Name).Execute(1);
                                if (landingPageResult.Any()) // did we get anything?
                                {
                                    foreach(var page in landingPageResult) // should loop only once because the query has a limit of 1
                                    {
                                        var landingPage = contentRequest.UmbracoContext.Content.GetById(int.Parse(page.Id)); // returns the IPublishedContent node
                                        targetLandingPage.Add(landingPage); // add to the empty container. the container was declared outside the if and foreach for scope purposes
                                    }
                                }
                            }
                // use the productPublishedContent class to merge the product node amd the parent node. This ensures that the virtual node is loaded as if it was an actual node that is part of the content tree
                            contentRequest.PublishedContent = new ProductPublishedContent(productPublishedContent[0], targetLandingPage[0]); // pass in the published nodes only. the containers are of List type
                        }
                    }
                }
                else
                {
                    return false;
                }
                return true;
            }
        }
    
        public class ProductPublishedContent : PublishedContentWrapped // inheriting from PublishedContentWrapped is what enables loading the virtual and parent nodes as one single PublishedContent object
        {
            private readonly IPublishedContent _productPublishedContent;
            private readonly IPublishedContent _targetLandingPage;
    
            public ProductPublishedContent(IPublishedContent productPublishedContent, IPublishedContent targetLandingPage) : base(productPublishedContent)
            {
                _productPublishedContent = productPublishedContent;
                _targetLandingPage = targetLandingPage;
            }
    
            public override IPublishedContent Parent
            {
                get { return _targetLandingPage; }
            }
            public override string Path
            {
                get
                {
                    return _targetLandingPage.Path + "," + _productPublishedContent.Id;
                }
            }
        }
    }
    

    I'm sure there's a better way and perhaps more elegant way to accomplish this.
    In the meantime. I'm just relieved that I got it to work.

  • Amir Khan 1287 posts 2744 karma points
    Jul 08, 2020 @ 01:00
    Amir Khan
    0

    This is awesome, thank you for sharing. I can see so many use cases for this logic.

  • Juan C Rois 6 posts 99 karma points
    Jul 08, 2020 @ 02:58
    Juan C Rois
    0

    The cherry on top is that I already had content wired up for the childPage. And since I'm using it as the parent for the virtual content, I did not have to override any properties or add anything.
    It just worked.

    Also, no need for custom routes or custom controllers.

    Thank you Amir for lending me your attention.
    Shoutout to Marc Goodson for pointing me in the right direction.

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    Jul 08, 2020 @ 20:26
    Marc Goodson
    1

    Great Juan!

    It's like magic isn't it? :-)

    I was just starting to explain up above, what you have already worked out! that you don't need to map a route, because you don't have a fixed pattern to your route, and your IContentFinder you have the power to construct your IPublishedContent from another ContentItem, based on the incoming url segments - so yes lots of messy stuff interpreting the Url, but glad you got it working!

    regards

    Marc

  • Juan C Rois 6 posts 99 karma points
    Jul 09, 2020 @ 14:18
    Juan C Rois
    2

    It was just like magic. I expected to tweak a few things here and there after getting the content on the page. But then the page loaded and everything was there. I went NO WAY! It was the best Eureka moment I've had in 12 years of development.

  • Puck Holshuijsen 184 posts 727 karma points
    Feb 12, 2024 @ 14:26
    Puck Holshuijsen
    0

    Thanks so much! This helped me a lot (even in Umbraco 13 :))

Please Sign in or register to post replies

Write your reply to:

Draft