Categories sorted in custom order with their own submenu and path in Jekyll, for my thermo site

24 min. read

Yet another site, this time for my project about Numerical Recipes applied to thermodynamics problems, http://nrthermo.tk.

Check out the portfolio post.

In the associated repo, I included three physics problems and my attempt to find ways to solve them using the Numerical Recipes libraries. Either I try several libraries for the same problem and compare the results, or I use different libraries to solve different parts of the same problem.

So, for this site, I wanted to have the three problems (Breit-Wigner, P-V diagrams and Gibbs curve) as menu items. Then, when you clicked them, a submenu should appear with pages explaining the libraries used or the different parts of the problem. The best way to understand this behavior is to visit the dedicated site. How to automate that with Jekyll?

It took me some time and experimentation with this tool that is still new to me (and I don't know any ruby or yaml), but finally I figured it out (there's a lot of information on Jekyll's site about how to make "blogs", but not much about making "sites"). I'm going to start with the problems I faced and then write up my solution.

tl;dr

Before I start, here's the solution (for the lazy, although it probably won't make sense until you read the whole article):

Menu items:

  • Use pages, not posts.
  • Save your pages in folders named after the categories you have in your site. Those will be your main menu items. Put the folders under the root folder so that your URLs have the structure siteurl/category/pagetitle.html
  • Since you can not use posts for what we want to do here, and pages don't have categories, create your custom categories list through a yaml hashed list. Add the hashed-list with your categories to your _config.yml file in the order you want them to appear in the menu, and with the structure "urlname": "Display Name". For example:
    
    categories:
      "breitwigner": "Breit-Wigner"
      "pvdiagrams":  "PV-Diagrams"
      "gibbs":       "Gibbs"
    
  • Code your main menu. You can add other items to it as well. For example:
    
    <ul class="wrapper" role="navigation">
      <li><a href="{{ site.url }}">Home</a></li>{% for cat in site.categories %}
      <li><a href="{{ site.baseurl }}/{{ cat[0] }}/">{{ cat[1] }}</a></li>{% endfor %}
      <li><a href="{{ site.baseurl }}/forkit">Fork it!</a></li>
    </ul>
    
  • Add an index.html file to each folder to avoid the "Forbidden" page when you click on a category in the menu.

Sub-menu items:

  • Add a date variable in your pages' Front Matter, and use it to sort the sub-menu items as you wish.
  • Make the loop over the sorted pages instead of the site.pages (Jekyll collection). For example:
    
    {% assign sorted_pages = site.pages | sort:"date" %}
    <ul class="submenu">
      {% for cat in site.categories %}
        {% if page.dir contains cat[0] %}
          {% for sorted in sorted_pages %}
            {% if sorted.dir == page.dir %}
      <li{% if page.url == sorted.url %} class="active"{% endif %}><a href="{{ sorted.url }}">{{ sorted.stitle }}</a></li>
            {% endif %}
          {% endfor %}
        {% endif %}
      {% endfor %}
    </ul>
    

To do: Strip the .html out of the generated URLs (permalink only works for pages if it is in the Front Matter of the page, and that breaks the whole concept of automation that I want to achieve here).

That's all. But to understand the WHY, keep reading.

First problem: how to stop Jekyll from sorting items alphabetically

I need the menu items sorted in a custom way rather than in alphabetical order. That means I want "P-V diagrams" after "Breit-Wigner", but "Gibbs" should come after "P-V diagrams". I also want to match them to categories.

In Jekyll, when you add a category to a post, it saves it in a collection called categories, which is a "hash" or mapping (kind of an array whose elements are made up of pairs of items, like a dictionary or collection). That is, for each category in the categories array, we'll have two items: category[0], which stores the category, and category[1], which stores the contents of the post. Every time you add a category: "something" variable to the Front Matter of a post, it is added to this collection.

The categories collection is never created for PAGES, even if you add a category variable to their Front Matter. It will just behave as any other variable.

The categories in this collection are sorted according to the date variable if it is present in the Front Matter, and if not, the date in the name of the post. For the same date, they are sorted alphabetically. That's not what I want.

So first I'm going to explain the case where I would use posts and make my own set of categories to add to the _config.yml file, to illustrate why that approach didn't work for this particular project.

Categories as an array


mycategories:
  - "breitwigner":
  - "pvdiagrams":
  - "gibbs":

This is not a hash, it's an array (only one item per array element).

Don't call your array categories because it is a reserved word for posts, and you won't be able to access your elements (site.categories will return the elements in Jekyll's collection or absolutely nothing if your site is made of only pages). If you define them as a hash instead, and you name it categories, it will add your categories to Jekyll's collection and you'll make a mess. So let's call it mycategories by now.

Now I add category: "categoryname" (for example) as a variable to the Front Matter of my posts. If I wanted to list the posts under each category (which I don't) I could do it like this:


{% for mycategory in site.mycategories %}
<h1>{{ mycategory }}</h1>
<ul>{% for post in site.posts %}{% if mycategory == post.category %}
    <li><a href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a></li>{% endif %}{% endfor %}
</ul>{% endfor %}

This would print each category according to my custom sorting, followed by the posts in that category.

In short, I have categories in site.mycategories and categories in site.categories, with the same category names, only that mycategories is an array and is sorted as I want, and categories is a hash and is sorted as Jekyll wants (alphabetically, by date, etc.).

Two comments:

  • How do I make the permalinks (the post.url) be /category/title? If I have my posts inside the _posts folder, the generated permalink is /category/YYYY/mm/dd/title.html. If my site is made of only pages instead, and they live in the root folder, the permalink (page.url in this case) is /title.html. So maybe I should switch to using pages instead of posts.
  • If I had a million pages under a category, do I have to manually type the category in every page or can I do something like category: {{ site.mycategories[0] }}? That way, if your category name changed and you were in a situation where you couldn't use find and replace, you wouldn't have to update a million pages. Mixing liquid syntax with the yaml syntax of the Front Matter can not be done in Jekyll unless you use a plugin. Unfortunately, pages hosted in GitHub are not allowed to use plugins.

The good news is that both things can be achieved if we use pages --instead of posts-- and match categories to folders. And so that is why I ended up using pages instead.

Second problem: Matching categories to folders to URL paths

The idea is to avoid adding a category variable in each and every file, so one possibility is to save the files under a folder that is named after the category it belongs to.


--breitwigner
  |--bw1.md
  |--bw2.md
  |-- ...
--pvdiagrams
  |--pv1.md
  |--pv2.md
  |-- ...
--gibbs
  |--gb1.md
  |-- ...

In doing so, I don't need to specify the category variable in the Front Matter anymore. I just have to save the file in the corresponding folder. Since I won't be generating categories in the Front Matter, no categories collection will be created by Jekyll. Also, my files will be treated as pages by Jekyll, because they are not under the _posts folder anymore. This means you could go on and name your array categories instead of mycategories in the _config.yml file.

For Jekyll, a file is a post only if it lives under the _posts folder and its name starts with a date. Any file that does not obey these two things is considered a page. This will tell you if you should use site.posts or site.pages in your loops. Post files always include the date in the generated URL structure (YYYY/mm/dd/title.html) unless you specify permalink: /:categories/:title in the _config.yml file and add a category variable in the Front Matter of every file. Also, if you have post files in sub-folders under _posts, those sub-folders will be stripped out when the site is generated and you will loose the folder structure.

My files are not acting as posts anymore but as pages under folders. Their URL will be /foldername/pagetitle.html which in this case matches /category/pagetitle.html. But if we click on a category, we will get a "Forbidden" error page. To fix this, we just have to add an index.html file under each folder. In my case, I use that file to introduce the physics problem at hand.

I still have to find a way to strip the .html out of the URL. Looks like the global permalink: /:categories/:title only works for posts. For pages, we have to add a permalink variable to their Front Matter, which means, typing the actual name of the category in each page. But, again, that would be against all the automation we are aiming for. Do you have any idea on how to achieve this?

Third problem: Having a "variable" name and a "display" name for each category

I don't like "breitwigner" appearing on the menu, "Breit-Wigner" would be much better. But "breitwigner" is a better name for a variable. How to have "variable" names and "display" names for categories?

That's easy. The array where all the categories are defined has to be turned into a hash collection, like this:


categories:
  "breitwigner": "Breit-Wigner"
  "pvdiagrams":  "PV-Diagrams"
  "gibbs":       "Gibbs"

Now we can list them automatically using a for over the categories. We can access the categories collection with site.categories, and then, for each element in that collection, we use the first item for the URL, and the second to display it as the menu-item text. I added a link to the home page and to the repos page. The final menu looks like this:


<ul class="wrapper" role="navigation">
  <li><a href="{{ site.url }}">Home</a></li>{% for cat in site.categories %}
  <li><a href="{{ site.baseurl }}/{{ cat[0] }}/">{{ cat[1] }}</a></li>{% endfor %}
  <li><a href="{{ site.baseurl }}/forkit">Fork it!</a></li>
</ul>

Fourth problem: sub-menus

Every time I click on a category item of the main menu, the index.html page is loaded introducing the physics problem. To load the other pages I link to them in a sub-menu. This sub-menu changes when I click a different category.

To implement this behavior, I have to first select the pages who are under a category at all (there may be a front page, an about page, etc. I don't want them), and from those I select the ones from a particular category and populate the sub-menu with them.

In other words: for each category in sites.categories, I have to check if the loaded page is in a directory whose name matches the category. If that's the case, I loop on site.pages and check which are in the same directory as the loaded page. I place those in the sub-menu of that page and ignore the rest:


<ul class="sub-menu">{% for cat in site.categories %}{% if page.dir contains cat[0] %}{% for p in site.pages %}{% if p.dir == page.dir %}
  <li{% if page.url == p.url %} class="active"{% endif %}><a href="{{ p.url }}">{{ p.title }}</a></li>{% endif %}{% endfor %}{% endif %}{% endfor %}
</ul>

I also added a class "active" as I have done in other Jekyll menus in the past. I still have two problems:

  • The items in the sub-menu are sorted alphabetically: that can be fixed by adding a date variable in the Front Matter of each page, and using the dates to sort the pages in the order we want them to appear (this is not like adding a category variable, because the date would be unique, so it would be like adding a title). Then, instead of looping on site.pages we loop on the sorted pages. To sort the pages by date, we add {% assign sorted_pages = site.pages | sort:"date" %} at the beginning of the sub-menu: {% assign sorted_pages = site.pages | sort:"date" %}.
  • The page titles are too big for the sub-menu items: I fixed that adding a variable stitle that stores a short title.

The sub-menu now looks like this:


{% assign sorted_pages = site.pages | sort:"date" %}
<ul class="submenu">
  {% for cat in site.categories %}
    {% if page.dir contains cat[0] %}
      {% for sorted in sorted_pages %}
        {% if sorted.dir == page.dir %}
  <li{% if page.url == sorted.url %} class="active"{% endif %}><a href="{{ sorted.url }}">{{ sorted.stitle }}</a></li>
        {% endif %}
      {% endfor %}
    {% endif %}
  {% endfor %}
</ul>

Other stuff (not related to menus)

For the equations I used MathML with MathJax. I also added a comment next to each equation with the equivalent in LaTeX syntax.

There are four different fonts used in this design, which makes me feel a bit ashamed. I will just say that I would have never done it had the site been non-static. Also, I optimized the loading, adding &text=NumericalRps and &text=THERMODYNAICS in the src attribute of the link tag to download just what I need.

I found it very useful to create an extra exclude folder to put everything I don't want tracked by git or generated by Jekyll. That way, instead of adding file by file, I can just add the folder to .gitignore and to the exclude variable in Jekyll's _config.yml file.

The code of the site can be found here.

Final remarks

I did this because I didn't want to write the category name in each file. However, I'm working in another site where I use posts instead of pages and add a category variable in the Front Matter, set permalinks, and use a fake date for the posts to sort them in the order I want. I'll be writing about that in the future!

But now, I need a drink.

Comments