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.
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.
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.
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.
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.
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:
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.
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.
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!
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.
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:
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.
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.
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.
They're annoying for sure but consistent.
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:
or at least that's the gist of it, your IContentFinder would return this hybrid object, instead of the Product IPublishedContent item.
regards
Marc
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.
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:
Step 3:
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.
This is awesome, thank you for sharing. I can see so many use cases for this logic.
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.
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
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.
Thanks so much! This helped me a lot (even in Umbraco 13 :))
is working on a reply...