Jekyll Tags on Github Pages

If you want to add tags to your Jekyll blog but found out that it is simply not supported by Github pages. You are at the right place here.

It is true that Github pages do not support customized Ruby plugins for Jekyll site. It is also true that the great jekyll-tagging is not one of Github default jekyll plugins.

As a result, we need to implement Jekyll tagging without plugins, it sounds hard, but here is a step-by-step guide to achieve so.

If you want to see live demo, you are already in a sample site that is hosted by Github, and has tags! Check out the source code: Github.

1. Add tags to your posts

In each of your post file (Markdown, presumably), you need to add the tags to the specific post in the front matter section. For example, for this post, the front matter looks like this:

---
layout: post
title: Jekyll Tags on Github Pages
description: blablabla
tags: jekyll blog github-page
---

In the tags entry, each tag is separated by white space. It is recommended to use lowercases for tag names.

2. Collect tags of all posts

What we need is a list named site.tags for our site. In order to do so, we can add a script to collect these tags using Liquid Templating of Jekyll.

In your _includes folder, add an html file called collecttags.html:

{% assign rawtags = "" %}
{% for post in site.posts %}
  {% assign ttags = post.tags | join:'|' | append:'|' %}
  {% assign rawtags = rawtags | append:ttags %}
{% endfor %}
{% assign rawtags = rawtags | split:'|' | sort %}

{% assign site.tags = "" %}
{% for tag in rawtags %}
  {% if tag != "" %}
    {% if tags == "" %}
      {% assign tags = tag | split:'|' %}
    {% endif %}
    {% unless tags contains tag %}
      {% assign tags = tags | join:'|' | append:'|' | append:tag | split:'|' %}
    {% endunless %}
  {% endif %}
{% endfor %}

This liquid scripts creates the list site.tags.

3. Execute the collection

You need to include the collecttag.html somewhere so that it is executed before site.tags are used.

My solution is to include it in head.html where Jekyll blogs defines the header. Google analytics script is also included here.

So, insert the following lines to _includes/head.html, inside head html section:

{% if site.tags != "" %}
  {% include collecttags.html %}
{% endif %}

This script runs the collecttags.html, and runs only once.

4. Display the tags of a post

We would like to display the tags of a post inside the post page, like this page. Then the modifications go to the format of posts: _layouts/post.

For example, the following code is inserted into the post layout:

<span>[
  {% for tag in page.tags %}
    {% capture tag_name %}{{ tag }}{% endcapture %}
    <a href="/tag/{{ tag_name }}"><code class="highligher-rouge"><nobr>{{ tag_name }}</nobr></code>&nbsp;</a>
  {% endfor %}
]</span>

I removed part of the style for this code snippets. You should adapt it to your own website design.

Note that a link is inserted to each of the tags of the post, at /tag/tag_name. We will come to that in the next step.

5. Generate the tag page

If you click on the tag displayed in this page, it will navigate you to a tag page, like this: hololens <- click it.

5.1. Define the layout of tag page

The tag page needs to be generated for all tags, because no plugin can be used to generate it when Github is building the page. Therefore, we treat the tag page as a kind of normal Jekyll page, with the only difference that it uses another layout.

OK. Now let’s create the layout of tag pages. Add another html file at _layouts/tagpage.html:

---
layout: default
---
<div class="post">
<h1>Tag: {{ page.tag }}</h1>
<ul>
{% for post in site.tags[page.tag] %}
  <li><a href="{{ post.url }}">{{ post.title }}</a> ({{ post.date | date_to_string }})<br>
    {{ post.description }}
  </li>
{% endfor %}
</ul>
</div>
<hr>

The above is the format of all tag pages. It displays all the posts that has the current tag, including post title, date and description.

5.2. Trigger the building of tag pages

In order to tell Github Jekyll engine to create a tag page, we need a markdown file specifying the specific tag to display and the above tagpage format.

Create a folder called tag in the root folder, and create a markdown file hololens.md like this:

---
layout: tagpage
title: "Tag: hololens"
tag: hololens
---

It triggers the building of a page at site_dir/tag/hololens/, with layout _layouts/tagpage.html, and parameter page.tag = hololens.

5.3. Automatic tag page creation (Optional)

If you would like to repeat step 5.2. for all the tags of your site, then you can skip this step.

Otherwise, I created a python script that crawls all the tags in the folder _posts/ and generate the desired tag page at tag/. It can be accessed here: tag generater.

You need to run this script before you push everything to Github repository and wait for your Github page.

6. Tag cloud display (Optional)

If you don’t want the tag cloud, you can skip this step.

Tag cloud can be like this: hololens <- click it (the bottom part). If you want such effect, you can also create the tag cloud without any plugin. You can change the size of the tag text depending on the number of posts related to it. Or you can display tags in the order of reference counts like my page.

What you need to do is to add another html page at _includes/archive.html:

<h2>Archive</h2>
{% capture temptags %}
  {% for tag in site.tags %}
    {{ tag[1].size | plus: 1000 }}#{{ tag[0] }}#{{ tag[1].size }}
  {% endfor %}
{% endcapture %}
{% assign sortedtemptags = temptags | split:' ' | sort | reverse %}
{% for temptag in sortedtemptags %}
  {% assign tagitems = temptag | split: '#' %}
  {% capture tagname %}{{ tagitems[1] }}{% endcapture %}
  <a href="/tag/{{ tagname }}"><code class="highligher-rouge"><nobr>{{ tagname }}</nobr></code></a>
{% endfor %}

This script fetches site.tags and sort them by the size of its referenced posts. The sorted list is in sortedtemptags. And it is iterated to be visualized. I am inspired by this stackoverflow answer.

You can add the following line to anywhere you want to display the tag cloud:

{% include archive.html %}

7. Push to Github

Done! Happy blogging!

If you like this post, please STAR my repository to motivate me!

Thanks for reading!


HoloLens File Transfer

Tranferring file to and from HoloLens seems non-trivial. It is written in HoloLens official document about saving and sending your files:

Unlike Windows on a PC or phone, HoloLens does not have a File Explorer application.

Getting access to non-media file is not an easy task. Tools like file picker and OneDrive are definitly over-kill for this task.

Goal

The goals here are two-folded:

  • Save something to a file in HoloLens application, and later access it
  • Create a file elsewhere, and make HoloLens application read it

File Explorer on Device Portal

File explorer on the device portal is used as a bridge. But the directories listed on the device portal is not complete. Therefore, we need to find out the accessible directory for both HoloLens application and user on PC.

Luckily I found such a location. The folder LocalAppData\SOMEAPP\RoamingState on the device portal is mapped to ApplicationData.Current.RoamingFolder.Path in the application at runtime.

Device Portal Screenshot

So, when the file is written to this path in UWP application, it will appear here, and you can download it by simply clicking on the download button. On the other hand, if you upload a file to this path, it can be read out by UWP application.

Scripts

The scripts are adapted from HoloToolkit.

Reading

The sample script for reading file is:

public void ReadString() {
  string s;
#if !UNITY_EDITOR && UNITY_METRO
  try {
    using (Stream stream = OpenFileForRead(ApplicationData.Current.RoamingFolder.Path, "filename.txt")) {
      byte[] data = new byte[stream.Length];
      stream.Read(data, 0, data.Length);
      s = Encoding.ASCII.GetString(data);
    }
  }
  catch (Exception e) {
    Debug.Log(e);
  }
#endif
  return s;
}

private static Stream OpenFileForRead(string folderName, string fileName) {
  Stream stream = null;
#if !UNITY_EDITOR && UNITY_METRO
  Task task = new Task(
    async () => {
      StorageFolder folder = await StorageFolder.GetFolderFromPathAsync(folderName);
      StorageFile file = await folder.GetFileAsync(fileName);
      stream = await file.OpenStreamForReadAsync();
    });
  task.Start();
  task.Wait();
#endif
  return stream;
}

Writing

The sample script for writing file is:

public void WriteString(string s) {
#if !UNITY_EDITOR && UNITY_METRO
  using (Stream stream = OpenFileForWrite(ApplicationData.Current.RoamingFolder.Path, "filename.txt")) {
    byte[] data = Encoding.ASCII.GetBytes(s);
    stream.Write(data, 0, data.Length);
    stream.Flush();
  }
#endif
}


private static Stream OpenFileForWrite(string folderName, string fileName) {
  Stream stream = null;
#if !UNITY_EDITOR && UNITY_METRO
  Task task = new Task(
    async () => {
      StorageFolder folder = await StorageFolder.GetFolderFromPathAsync(folderName);
      StorageFile file = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
      stream = await file.OpenStreamForWriteAsync();
    });
  task.Start();
  task.Wait();
#endif
  return stream;
}

In the above sample code, the file that is queried must exist. In the case when you want to list existing files in a given folder, it is as simple as:

StorageFolder folder = await StorageFolder.GetFolderFromPathAsync(folderName);
foreach (StorageFile f in allFiles) {
  Debug.Log(TAG + ": found file " + f.Name + ", " + f.DateCreated);
}

The above code snippets are tested with the following configurations:

  • Unity 5.5 + Visual Studio 2015
  • Unity 5.6 + Visual Studio 2017

Further Reading

Apart from the RoamingState folder on HoloLens Device Portal, some other folders are accessible as well, for example, TempState and LocalState etc. More information can be found on UWP API page of Windows.Storage.ApplicationData.

Thanks for reading!