While building a client’s site recently I was tasked with creating a dynamically generated section of the site based on data pulled down from an API. The section has one listing page and potentially unlimited subpages of which I don’t know the URLs. It basically looks like this:
Event Listing (lists all available events) -> Event page (dynamic page generated from event selected)
I toyed with the idea of using the Umbraco content creation functionality and pull the data into Umbraco but I realized this could cause problems if someone edited the content in Umbraco but it was then overwritten when any changes were made on the API side of things.
After a bit of research I came across the IContentFinder. This allows you to add your own custom URL routing rules into the request pipeline.
Using this I can intercept any requests coming into the dynamic section of my site and send them onto the appropriate template I have set up to generate the subpages. The nice thing about using IContentFinder is that we can keep the URL structure of the site the same but don’t have to create content for each subpage in Umbraco for that URL to be routed to, it’s all handled by our routing rules and templates so it doesn’t matter that we don’t know the URLs.
The first thing to do it create our content in umbraco, this is so we have something to route to which can then decide to generate our listing or details pages. I simply created a node under my home node with and assigned the ‘EventListing’ document type to it.
Next we need to implement the IContentFinder interface and create the method TryFindContent which, predictably will try to find our chosen content and return a Boolean based on the result. My method grabs the Umbraco node which is the parent of the dynamic section, checks if the incoming request URL is asking for its subpages, if it is, we assign the node to the request and returns the result.
Mine looks something like this, it’s in the App_Code folder:
public class ContentFinder : IContentFinder { public bool TryFindContent(PublishedContentRequest contentRequest) { var allNodes = uQuery.GetNodesByType("EventListing").OrderByDescending(n => n.Level); foreach (var node in allNodes) { string fullUri = contentRequest.Uri.AbsolutePath; string parentUri = node.Url; bool isChild = fullUri.StartsWith(parentUri, StringComparison.InvariantCultureIgnoreCase); if (isChild) { contentRequest.PublishedContent = new UmbracoHelper(UmbracoContext.Current).TypedContent(node.Id); return true; } } return false; } }
Next we need to register this when the CMS starts so Umbraco knows to use our content finder. This next part took a fair bit of messing about as there seems to be a lot of conflicting information on different version of Umbraco. For Umbraco 7 which I’m using this seems to be the way to do it.
We need to extend ApplicationEventHander and override the ApplicationStarting event to register our content finder. Mine looks like this, its in the App_Code folder:
namespace Umbraco.Extensions.EventHandlers { public class DynamicPageRoutes : ApplicationEventHandler { protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { base.ApplicationStarting(umbracoApplication, applicationContext); ContentFinderResolver.Current.InsertType<ContentFinder>(); } } }
So now if our request is meant for the dynamic section we have set the content node it will resolve to. Next we need to create a controller for the document type of that node to route the request to either the listing page or the subpage. Mine looks like this, it’s in the App_Code/Controllers folder.
public class EventListingController : RenderMvcController { public override ActionResult Index(RenderModel model) { string nodeUrl = model.Content.Url; string currentUrl = Request.Url.AbsolutePath; currentUrl = currentUrl.TrimStart('/'); string[] urlSegments = currentUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (urlSegments.Length > 1) { return View("Event", new EventRenderModel(model, urlSegments)); } return base.Index(model); } }
Finally you need a model to pass to the view, mine looks like this in the App_Code/Models folder:
public class EventRenderModel : RenderModel { public string[] Parameters { get; set; } public EventRenderModel(RenderModel model, string[] parameters) : base(model.Content, model.CurrentCulture) { Parameters = parameters; } }
Done! This looks complicated at first but once you get your head around the code it makes sense I promise!