Minimum Viable Product - Part 5 - Tags

Tag you're it

Up to this point in our series we've learned about data types, document types and templates. Today we're going to look at one aspect that cuts across all of those topics — tags.

Tags are a great way to organize blog content by associating one or more words that can be used as quick search terms. When a user clicks on a tag, they can be shuttled to a page that shows other articles with that tag. Tagging by the writer is easily done as we have already added the 'tags' data type to our BlogPostPage document type as we can see in the image below:

Umbraco has built-in tag support

Tagging in Umbraco goes beyond the normal data type, it has it's own table in the DB and it even has it's own API. There is also tag accessibility from views via the Umbraco helper, here are some examples of how to get some tagging information:

//from a .cs file
var tagService = ApplicationContext.Current.Services.TagService;

//from a view
@inherits UmbracoTemplatePage
@{
    var list = Umbraco.TagQuery.GetContentByTag("foo");
}

All your tags are belong to me

In order to have a page to show all similarly tagged articles, we need a few ingredients:

1) Place link tags on each article and point them to our 'tag' page that lists out the articles, the URL format will be this: '/blogs/tag/{tag-name}'.

2) Create the 'tag' page that does the listing (example here)

3) Fiddle with some code to make the URL routing happen the way I want it to

The last part about URL's is important due to how I want everything to work. First things first, I didn't want to use a query string for the tag value (i.e. /blogs/tag?t=umbraco). If I did, then things would get a lot easier. However since I'm a URL snob, I want my links to look like this instead: /blogs/tag/umbraco.

Making the URL's look nice comes with some consequences we will have to deal with. By default, Umbraco will try to route a request to the blogs node then look for a node named tags as a child of blogs then a node named umbraco as a child of tags. The result will be a 404 since our content structure does not resemble that. Let's see a quick peek of how our content is structured again:

Notice in the image above, our tag listing page is right under our 'home' page which makes its URL '/tag/'. This will obviously be a problem if we're trying to use a URL like '/blogs/tag/umbraco'. We could move the 'tag' page under 'blogs' so that it will get the URL of '/blogs/tag'. Closer, but still not there unless we create a child for every tag under the tag node so that the Umbraco routing works properly by default. Creating a piece of content for every tag just to get the routing to work right creates double work for the editor, so let's do something else.

To solve the problem of having the 'tag' page in the wrong spot and to get the URL exactly the way we want it, we will use Umbraco's secret sauce interface IContentFinder. This interface will allow use to create a bit of code that will intercept this request and route the request properly. There are many good articles on content finders if you'd like to know more about them along with the official documentation.

Content finders get registered during startup. Once registered, a content finder gets the opportunity to examine the incoming request and select the correct piece of content to serve to the user. It's up to the content finder to determine whether or not it should handle the request, or simply pass it to the next content finder. If the content finder decides to set the content, no more content finders will get the opportunity to examine the request.

We will create a content finder that will handle URL's of the form '/blogs/tag/{tag-name}'. If a URL comes in with that format, we will then serve the 'tag' page which is really located at '/tag'. The browser address will remain what we want as this isn't a true redirect. Additionally, we will 'remember' the tag so that it can be used in a tag query on the view. Let's look at some code:

using System.Web;
using Umbraco.Core;
using Umbraco.Web.Routing;

namespace KGLLC.Umbraco.ContentFinders
{
    //this class registers our content finder
    public class TagContentFinderRegistration : ApplicationEventHandler
    {
        protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            //register our tag finder
            ContentFinderResolver.Current.InsertTypeBefore<ContentFinderByNotFoundHandlers, TagContentFinder>();
        }
    }

    //this is the actual finder
    public class TagContentFinder : IContentFinder
    {
        public bool TryFindContent(PublishedContentRequest contentRequest)
        {
            //break apart the request url
            var paths = contentRequest.Uri.LocalPath.Trim('/').Split('/');

            //broad stroke to see if we are the right content finder
            if (paths.Length == 3 && paths[1] == "tag")
            {
                //set the tag for use later downstream
                HttpContext.Current.Items["tag"] = paths[2];

                //grab the tag page, it could have been anywhere we wanted to put it
                var tagPage = contentRequest.RoutingContext.UmbracoContext.ContentCache.GetByRoute("/tag/");

                //make sure we actually found it
                if (tagPage != null)
                {
                    //set the request to the tag page
                    contentRequest.PublishedContent = tagPage;

                    //lets Umbraco know we found it, no further content finders will run
                    return true;
                }
            }

            //let Umbraco know we're not the droid it's looking for, the next content finder will get the chance to examine the request
            return false;
        }
    }
}

TagPage Template

Now that we're handling requests properly, we need to implement our TagPage view. In the view we determine if there is a tag set from our content finder, then we will use an Umbraco tag query to list out the articles:

@using KGLLC.Umbraco.Helpers
@inherits UmbracoTemplatePage
@{
    Layout = "Base.cshtml";

    //gets the tag that was set in the content finder
    var tag = HttpContext.Current.Items["tag"];

    //if there is no tag, send to 404
    if (tag == null)
    {
        Response.Redirect("page-not-found");
        return;
    }

    //crudely handles if there are spaces in the tag
    //the urls will have spaces replaced with dashes so this undoes it
    var tagString = tag.ToString().Replace("-", " ");
}

<section id="main" class="blog">
    <div class="container">
        <div class="row">
            <div class="col-md-8">
                <h2>
                    Items tagged as "<strong>@tagString</strong>":
                </h2>
                @* this is the magic that gets content by tag *@
                @foreach (var blog in Umbraco.TagQuery.GetContentByTag(tagString).OrderByDescending(x => x.ToPublishedDate()))
                {
                    @(Html.Partial("~/Views/Partials/BlogTease.cshtml", blog))
                }
            </div>
            <div class="col-md-4 right-column">
                @Html.Partial("~/Views/Partials/RecentBlogList.cshtml")
                @Html.Partial("~/Views/Partials/Portfolio.cshtml")
            </div>
        </div>
    </div>
</section>

Summary

So that's just touching the surface with tags and content finders with Umbraco. It's quite amazing that you can manipulate the URL's, tag and retrieve content easily. Next up we will be setting up our simple contact form.