Adding a New Tags Page to Ghost

One of the things I always felt that was missing from Ghost was the lack of a global tags page listing out all of the tags on a particular blog, so I built one out for the blog:

Tags
This is the notebook of an engineer, filled with tutorials, notes, and projects.

(You can also find the link under "Tags" at the top of this post).

Personally, I find the traditional "category list"-style tags list a little too stiff/boring. It probably has its place in a sidebar with the table of contents, but my typical post is far too long and therefore that real estate is occupied. So, since I am creating a new page template anyway, I might as well go with the style I like.

One of my favorite data visualizations is the tag cloud. It's useful not only for your audience to be able to see the topics you write about, but it also helps you as a writer/blog owner to see where your topics are concentrated since tag clouds provide a visualization where individual tags are weighted based on their number of posts.

The Design

In order to create the cloud tag page, I started by laying out exactly what I wanted: a list of all my tags, in alphabetical order, with font size weighted by the number of posts associated with that tag. Additionally, to add emphasis, I wanted to weigh the color of the font based on that same number of posts in a given tag.

To obtain the list of tags, thankfully Ghost exposes "tag" as a context to Handlebars.js. We can then simply use a "get" helper in Handlebars to query the tag data, followed by a "for each" loop to obtain each individual tag (as well as the number of posts associated with it).

In order to weigh the individual tags, I knew we would also need the number of posts in the largest tag (the "max") and the number of posts in the smallest tag (the "min"). Once we had an absolute max and min for the tag dataset, we could then write a function to calculate the relative weighting of each tag.

The Algorithm

This presented several options for how to best perform this weighting. No matter what, we know we have to iterate over each tag just to generate the necessary span/a href link tag list. That's a fixed cost in this project. The only questions that then remain is, "How best to obtain the max/min?" and "When should I weigh the tags?".

How Best to Obtain the Min/Max

Since we have the fixed cost of having to iterate over each tag in the for each loop, it first crossed my mind that this would also be the time to obtain the min and max for this data set. The problem with this idea is that if I did do so, I wouldn't have the min and max until after I had already iterated over each tag, which would then require an additional loop over each tag to weigh them, thus wasting the opportunity I had in my initial pass over the data set.

Therefore, it makes the most sense to obtain the max and min prior to generating the tag list in the "for each" loop, especially since the max/min can be obtained by the server via a SQL query on the backend (which should be the fastest from a performance standpoint):

{{#get 'tags' limit='1' include='count.posts' order='count.posts asc'}}
	{{#foreach tags}}
		<script>
			var min = {{count.posts}};
		</script>
	{{/foreach}}
{{/get}}
{{#get 'tags' limit='1' include='count.posts' order='count.posts desc'}}
	{{#foreach tags}}
		<script>
			var max = {{count.posts}};
		</script>
	{{/foreach}}
{{/get}}

The semi-clever get helper above queries the tags and sorts by the number of posts in either ascending or descending order, depending on if we want the min or the max. Since we want only the absolute min/max, we then limit the query to 1 tag each and store that count off in a variable for JS to use later.

When To Weigh The Tags

The above algorithm decision then makes the decision for when to weigh the tags for us. We now have the max and min, we should then calculate the constants for the general weight function outside of the for loop (so we aren't calculating the same constants every single time within the "for each" loop):

<script>
	const maxFontSize = 30;
	const minFontSize = 12;
	const maxColor = 255;
	const minColor = 135;
	const slopeSize = ((maxFontSize - minFontSize)/(max - min));
	const slopeColor = ((maxColor - minColor)/(max-min));
	const sizeB = (minFontSize-(slopeSize));
	const colorB = (minColor-(slopeColor));
</script>

And finally, we perform the actual formatting of the tags list in our "tag cloud":

{{#get 'tags' limit='all' include='count.posts' order='name ASC'}}
	{{#foreach tags}}
		<span class="tag-cloud" id="tag-{{ name }}">
			<a href='{{ url }}'>{{ name }}</a>
		</span>
		<script>
			var size = (slopeSize*({{count.posts}}) + sizeB);
			var spanColor = (slopeColor*({{count.posts}})+(colorB));
			document.getElementById('tag-{{ name }}').style.fontSize = (size + "px");
			document.getElementById('tag-{{ name }}').childNodes[1].style.color = "rgb(" + spanColor + ", " + spanColor + ", " + spanColor + ")";
		</script>
	{{/foreach}}
{{/get}}

The above code loops over each tag and creates a span of class "tag-cloud" with an id of "tag-{{TAG NAME HERE}}". This ID allows us to select only that particular span and format it with JavaScript:

var size = (slopeSize*({{count.posts}}) + sizeB);
var spanColor = (slopeColor*({{count.posts}})+(colorB));
document.getElementById('tag-{{ name }}').style.fontSize = (size + "px");
document.getElementById('tag-{{ name }}').childNodes[1].style.color = "rgb(" + spanColor + ", " + spanColor + ", " + spanColor + ")";

The size and spanColor calculations are standard linear equations that calculate a color and size to weigh each tag relative to the max/min. (This is also why I left the calculations for the constants in the JavaScript further up in the body. That way it would be easier to readers to be able to modify the code to suite their own needs and see what's going on).

Critical Reflection

When you create a page like this tag cloud, the shortcomings of not controlling your own backend become readily apparent. For one, there's no reason we should have to put off so much of this processing on the front end: the backend knows how many posts are in each tag, why pass those to the client and force the client to calculate and format the tags with JavaScript? Also, how often could the tag cloud possibly change? A few times a week at most? It's a static page, why do all this work for every single user that requests the page?

So, while I'm happy with my new nightCasper theme as it stands right now, there is certainly room for optimization. I have a few other projects I need to put on the burner now, but I'll be coming back to this.


If you have any questions or just wish to argue, I look forward to it in the comments below!