Wednesday, 16 October 2013

Orchard CMS Taxonomy Term Autoroute Token Slug

Edit 17/10/2013:
Refer to on updates regarding this feature.

Edit 11/11/2013:
Since writing this article I've discovered a better method which allows a full taxonomy term path to be slugified and returned into a token result:

Recently (as of 17/10/2013) the Orchard CMS team has fixed a bug which was preventing taxonomy tokens from returning a value.

Currently I am working off changeset a62f0281ebdac780a1d3a583dbc5821ca8fbc6ce on version 1.7.1 and can confirm that this is working.

An example of an autoroute pattern that works is {Content.Fields.BlogPost.Category.Terms:0}, where [BlogPost] is the name of your content type, and [Category] is the name of the field within our content type, not the taxonomy or term.

This means we can start injecting taxonomy terms into our autoroute patterns and enjoy structuring a url hierarchy based on taxonomy terms and being able to interact with this new url data within queries and layer rules. This is fine if your taxonomy is a single word containing no spaces or special characters, but once you introduce other characters these are also injected into the URL.

The following code adds a new token called TermSlug which allows a taxonomy term to be slugified:

  • File to modify: \src\Orchard.Web\Modules\Orchard.Taxonomies\Tokens\TaxonomyTokens.cs
  • Usage: {Content.Fields.BlogPost.Category.TermsSlug:0} (refer to example above)
// Comment
using System;
using System.Linq;
using Orchard.Taxonomies.Fields;
using Orchard.Localization;
using Orchard.Tokens;
using Orchard.Autoroute.Services;

namespace Orchard.Taxonomies.Tokens {
    public class TaxonomyTokens : ITokenProvider {
        private readonly ISlugService _slugService;

        public TaxonomyTokens(ISlugService slugService)
            T = NullLocalizer.Instance;
            _slugService = slugService;

        public Localizer T { get; set; }

        public void Describe(DescribeContext context) {
            // Usage:
            // Content.Fields.Article.Categories.Terms -> 'Science, Sports, Arts'
            // Content.Fields.Article.Categories.Terms:0 -> 'Science'

            // When used with an indexer, it can be chained with Content tokens
            // Content.Fields.Article.Categories.Terms:0.DisplayUrl -> http://...

            context.For("TaxonomyField", T("Taxonomy Field"), T("Tokens for Taxonomy Fields"))
                   .Token("Terms", T("Terms"), T("The terms (Content) associated with field."))
                   .Token("Terms[:*]", T("Terms"), T("A term by its index. Can be chained with Content tokens."))
                   .Token("TermsSlug[:*]", T("Terms"), T("A term slug by its index. Can be chained with Content tokens."))

        public void Evaluate(EvaluateContext context) {

                   .Token("Terms", field => String.Join(", ", field.Terms.Select(t => t.Name).ToArray()))
                       token => token.StartsWith("Terms:", StringComparison.OrdinalIgnoreCase) ? token.Substring("Terms:".Length) : null,
                       (token, t) => {
                           var index = Convert.ToInt32(token);
                           return index + 1 > t.Terms.Count() ? null : t.Terms.ElementAt(index).Name;
                       token => token.StartsWith("TermsSlug:", StringComparison.OrdinalIgnoreCase) ? token.Substring("TermsSlug:".Length) : null,
                       (token, t) =>
                           var index = Convert.ToInt32(token);
                           return index + 1 > t.Terms.Count() ? null :  _slugService.Slugify(t.Terms.ElementAt(index).Name);
                // todo: extend Chain() in order to accept a filter like in Token() so that we can chain on an expression
                   .Chain("Terms:0", "Content", t => t.Terms.ElementAt(0))
                   .Chain("Terms:1", "Content", t => t.Terms.ElementAt(1))
                   .Chain("Terms:2", "Content", t => t.Terms.ElementAt(2))
                   .Chain("Terms:3", "Content", t => t.Terms.ElementAt(3))

Hope this helps!