Even though Jekyll is a “static” site generator, with a little ingenuity you can give add many rich features to the generated site.

I wanted a tag-cloud! A good tag cloud. You know, one where the tags showed the number of posts next to them as well as growing in size according to the number of posts. One like you see to the left or in the lead image of this post.(yay!)

I also wanted a tagging system that allowed me to combine tags for posts and tags in Jekyll collections: the built-in Jekyll category and tag system doesn’t currently handle tags in collections.

To create an across all posts and an across all collections tag system just requires a name other than ‘tags’. You’ll use that custom-tag name in front matter array just as you would add the standard tags to a post.

Using a custom tag system does lead to some extra looping as you’ll see below. As this tagging system does not use plugins, I believe it could work on a GitHub deployment… no promises there.( I would love to hear if it works at GitHub, though )

Issues and Apologies

Everything I’ve learned and all my gut wants me to use a hash-map, create the view-partial only once, and keep the time complexity, “big O” to a reasonable level.

This solution, forced by the limitations of Jekyll and Liquid in their pursuit of simplicity, loops over every post numerous times. There is the nested loop of looking at every content item for each unique tag after we’ve also loop through all the posts to find unique tags. Clearly a hash tags as keys and values comprised of arrays of associated posts would take a single pass with a second pass to render the tags of various sizes and item counts. If I had used their tag system it could have been a few steps more efficient(but again, that wouldn’t work for collection-tags)

Even worse, (yes it gets worse), the generator recreates the the tag map for each page….we’re almost looking at O cubed.

Why haven’t I found a work around for that? I could use a plugin to find the unique tags and set an environmental variable for that. There are also techniques to generate the tag-cloud once using rake or gulp, or even just generate a static page onto a blank layout normally and cutting and pasting it into an _include. To some degree, the simplicity is part of the Jekyll philosophy. These posts might stimulate thoughts on better work-arounds and I’d love to hear ideas. Generate an _include file Once at StackOverflow and Define and Compute custom Variables at the Jekyll Github Repository.

It would be really nice to be able to us a plugin generate an “_include” or YAML data file.

In the end, this current method will work with the number of posts I anticipate making while maintaing the basic Jekyll work-flow. Because we’re generating a static site none of the redundant calculations will effect client experience, there is no database load(as there isn’t one), and it is certainly more simple for non-developers to use. If the number of posts scaled to the point that the generation was impeding workflow, it would be easy enough to comment out the tag-cloud generation for all but the times you were going to push your new content to a production server.

On to the code:

The comments should explain how it works:


<!-- Create empty arrays to push to -->
{% assign many_tags = '' | split: ',' %}
{% assign unique_tags = '' | split: ',' %}

<!-- get all tags -->
{% for c in site.collections %}
  <!-- Map and flatten -->
  {% assign collectionTags =  c.docs | map: 'subject-tags' | join: ',' | split: ',' %}
  <!-- Push to tags -->
  {% for tag in collectionTags %}
    {% assign many_tags = many_tags | push: tag %}
  {% endfor %}
{% endfor %}

<!-- Get Unique subject-tags -->
{% assign unique_tags = many_tags | uniq | sort  %}

<div class="tag-cloud">
 <!-- go through each tag, find content-items with that tag -->
{% for cloud-tag in unique_tags %}
  <!-- initiate an array to hold related content-items -->
  {% assign tags_posts = '' | split: ',' %}
  <!-- go through every collection (posts  is also a collection) -->
  {% for c in site.collections %}
    <!-- every item in that collection -->
    {% for content-item in c.docs %}
      <!-- every subject-tag that a content-item has -->
      {% for subtag in content-item.subject-tags %}
        <!-- check if this content item matches the current tag and if so push-->
        {% if subtag == cloud-tag %}
          {% assign tags_posts = tags_posts | push: content-item %}
        {% endif %}
      {% endfor %}
    {% endfor %}
  {% endfor %}
  
  <!-- check number of content-items (note, trying simple numeric index had some issues with liquid )-->
  <!-- as you make more posts you will want to change the thresholds for size to your taste -->
  {% assign tag-quant = tags_posts | size %}
  {% if tag-quant < 3 %}
    {% assign tag-cloud-size =  "tag-cloud-size-1" %}
  {% elsif tag-quant < 5 %}
      {% assign tag-cloud-size =  "tag-cloud-size-2" %}
  {% elsif tag-quant < 8 %}
      {% assign tag-cloud-size =  "tag-cloud-size-3" %}
  {% elsif tag-quant < 12 %}
      {% assign tag-cloud-size =  "tag-cloud-size-4" %}
  {% elsif tag-quant < 18 %}
      {% assign tag-cloud-size =  "tag-cloud-size-5" %}
  {% else %}
      {% assign tag-cloud-size =  "tag-cloud-size-6" %}
  {% endif %}

  <!-- Now generate HTML for the given tag -->
  <!-- The links are to an index page that uses very similar code -->
  {% if tag-quant > 0 %} <!-- catch corner case of blank element in array -->
    <a href="/site-index.html#{{ cloud-tag|slugify }}">
      <span class="tag-cloud-full-tag" >
        <span class="tag-cloud-title {{tag-cloud-size}}">{{cloud-tag}}</span>
        <span class="tag-cloud-quantity">({{tag-quant}})</span>
      </span>     
    </a>    
  {% endif %}
{% endfor %}<!-- do it again for the next tag - Yes, holy-loop-ville -->

<!-- NOTE - remove all comments as dozens of them will show up in the generated HTML -->
</div>

Yes, there are a lot of loops in there, and like I mentioned in the preceding section, we do this for every page with a tag cloud on it. If I were going to have thousands of pages I would create a static page, with or without a plugin, and do the cut and paste hack I mentioned.

There are also a number of contortions to fit Liquid quirks like the technique of initiating the empty arrays to push to and the use of finding the size of an array of objects rather than an incrementing variable.

You’ll note that the tags are links to the tag location on an index page.

CSS:

.tag-cloud{
    margin: 0px 0px 0px -5px;
    position: relative;
}

.tag-cloud-full-tag{
    transform: scale(1.0);
    transition: all 525ms ease-out;
    display: inline-block;
    padding: .1em;
    border-radius: 4px;
    position: relative;
}

.tag-cloud-full-tag:hover{
    transform: scale(1.35);
    background-color: $main-background;
    opacity: 1;
    z-index: 600;   
}

.tag-cloud-title{
    padding: 0px;  
}

.tag-cloud-size-1{
    font-size: .7em;
} 
.tag-cloud-size-2{
    font-size: .9em;
} 
.tag-cloud-size-3{
        font-size: 1.1em;
    } 
.tag-cloud-size-4{
    font-size: 1.25em;
} 
.tag-cloud-size-5{
    font-size: 1.5em;
} 
.tag-cloud-size-6{
    font-size: 1.7em;
}

.tag-cloud-quantity{
    font-size: .5em;
    margin-left: -.2em;
    z-index: 700;
}

Do you want the index page too?


---
layout: side-nav
page-label: Site Index
permalink: /site-index.html
---
{% assign many_tags = '' | split: ',' %}
{% assign unique_tags = '' | split: ',' %}
{% for c in site.collections %}
  {% assign collectionTags =  c.docs | map: 'subject-tags' | join: ',' | split: ',' %}
  {% for tag in collectionTags %}
    {% assign many_tags = many_tags | push: tag %}
  {% endfor %}
{% endfor %}
{% assign unique_tags = many_tags | uniq | sort  %}
{% for index-tag in unique_tags %}
  {% assign tags_posts = '' | split: ',' %}
  {% for c in site.collections %}
    {% for item in c.docs %}
      {% for subtag in item.subject-tags %}    
            {% if subtag == index-tag %}
          {% assign tags_posts = tags_posts | push: item %}
        {% endif %}
      {% endfor %}
    {% endfor %}
  {% endfor %}
{% assign tag-quant= tags_posts | size %}
  {% if tag-quant > 0 %}
    <div id="{{ index-tag|slugify }}" class="subject-tag-container well well-sm" >
      <div class="site-index-label">
        <h2>
          {{index-tag}} <span class="badge">{{ tags_posts | size }}</span>
          <a href="#{{index-tag|slugify}}">
            <i class="site-index-target-toggle fa fa-plus-square pull-right" aria-hidden="true"></i> 
          </a>     
        </h2>
      </div> 
      <table>
        {% for content-item in tags_posts %}
          <tr>
            <td>
              <img src="../images/{{content-item.lead-image}}" alt="">
            </td>
            <td>
              <a href="{{content-item.url}}">
                <div class="site-index-title">
                  {{content-item.post-title}}
                </div>
                <div class="site-index-subtitle">
                  {{content-item.subtitle}}
                </div>     
              </a>
            </td>
          </tr>
        {% endfor %}
      </table>
    </div>
  {% endif %}
{% endfor %}


And then there is the SCSS (not currently in pure CSS form) Note the use of the :target pseudo element.

.site-index-label{
    h2{
        padding-top: 0px;
        margin-top: 0px;
        .site-index-target-toggle{
            display: initial;
            font-size: .7em;
        }

    }
        
}

.subject-tag-container{
    width: 300px;
    font-size: .9em;
    transition: all 525ms ease;
    background-color: $main-background;
    display: inline-block;
    
    li{
            list-style-type: none;
    }

    img{
        display: none;   
    }
    &:target{
        box-shadow: 5px 5px 5px $title-text;
        width: 600px;
        font-size: 1.2em;
        a{
            color: $link-two;
            &:hover{
                color: $link-hover;
            }

        }
        img{
            display: initial;
            width: 80px;
            padding: 10px 0px;
        }
        
        .site-index-target-toggle{
            display: none;
        }
    }
    tr{
        padding-bottom: 1em;
    }
    

    .site-index-title{
        // font-size: .8em;
        padding-left: 1em;
    }
    .site-index-subtitle{
        font-size: .8em;
        padding-left: 2em;
    }
}

….for a completely cheesy finish:

Hover over this:

That's All Folks