A while back I wrote about using Vorto, the popular multilingual property editor for Umbraco, with a custom content finder class to serve multilingual content within a single domain (see Building multilingual sites in Umbraco with Vorto). In this post, I will explain how one can use a custom content finder to display a node of content in more than one location.
Let's say that you have a site that has category and subcategory pages to organize content. For instance, you might have a structure that looks like this:
Within each subcategory, you might want to have book titles. If you had a page for Hegemony or Survival by Noam Chomsky, you would create it under the Non-fiction node as follows:
However, you may want to have the same content also exist under Paperback and Our Picks. This would allow the book to have the following URLs:
You could, of course, copy the node so it exists under each subcategory it belongs. But that is a bad idea because you would have duplicate copies of the same content and each content edit would require one to edit it in multiple places. An alternative approach is to create a custom content finder. (Note: it is generally bad for SEO to have the same content on multiple locations. You can specify canonical URLs in a link tag to avoid this problem.)
Content finders are how Umbraco knows what content to display on the front-end of the web site. By default, when an incoming HTTP request comes in, Umbraco displays content based on the node structure. For instance, if there is a request for /genre/science-fiction, the response contains the rendered content for the Science Fiction node under the Genre node. This content is found by the default content finder, ContentFinderByNiceUrl.
With Umbraco it is easy to write your own content finder. Content finders implement the IContentFinder interface. The interface has one method:
bool TryFindContent(PublishedContentRequest contentRequest)
This method returns true if content is found and assigns an Umbraco document to the content request. Implementing IContentFinder enables you to create your own logic for determining what content should be returned in the response.
To continue our example, say that all books are child nodes of a separate node outside of the two-level category structure presented above and that each node has a relation to one or more subcategory. The relation could be defined via a multi-node picker. We could then create a content finder that implements this logic:
- Split the request into two parts, the category part (/genre/non-fiction) and the book part (/hegemony-or-survival)
- Get the IPublishedContent objects for each part (one will be the subcategory node for Non-fiction and the other will be the book node.
- If the nodes are related (the book object contains a reference to the subcategory), then assign the book's IPublishedContent object to the content request and return true. Otherwise, return false.
In order for a custom content finder to get used by Umbraco, it must be added to the current ContentFinderResolver. An Umbraco application can have multiple content finders. It will try one, and if content is not found, then it will try the next one. I think be default, Umbraco comes with a few including ContentFinderByNiceUrl and a 404 Not Found content finder. The order they are added matters, and you can remove existing content finders. This should be done in an ApplicationStarting event handler. See the Umbraco documentation at https://our.umbraco.org/Documentation/Reference/Routing/Request-Pipeline/IContentFinder#example-wire-up for an example.
When implementing a custom content finder, you will likely also need to implement a custom url provider. A url provider is used by Umbraco to render links. A url provider implements IUrlProvider and has two methods:
string GetUrl(UmbracoContext umbracoContext, int id, Uri current, UrlProviderMode mode) IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current
The GetUrl() method is what Umbraco calls to get the URL when rendering content. When implementing this URL you can access the UmbracoContext and return the URL based on the context. This allows you, for example, to return a URL based on the current request location. In our book example, you could render a link to a book based on what category they are viewing. For instance, when viewing /genre/non-fiction, the link to our book would be/genre/non-fiction/hegemony-or-survival. But when viewing /popular/our-picks, it would link to /popular/our-picks/hegemony-or-survival. GetOrtherUrls() is used to generate the list of other URLs the content might which can be viewed in the properties tab of the content node in the back office.