Getting the keys of a dictionary in Liquid?

I’ve been struggling to improve the way my site uses categories. site.categories seems to be a dictionary (hash?) of "<category name>"=>[array of Jekyll::Document], and if I naively render a category in my page with {{ category }}, I get the fully-rendered and formatted post, which is surprising (i.e. it’s pulling in all the layout and HTML of my theme to render the post).

In any case, I want to do some things like render a naturally-sorted list of category names. For this, it seems I need to get the keys of the site.categories dictionary, but I can’t figure out how to do that. In more sophisticated languages I could do something like site.categories.keys or site.categories.map { $0.0 }.

Is there a way to get the keys of a dictionary in Liquid?

Have you tried to adapt the code from the documentation page?

Yes, of course. Exploring further, I’m not seeing what the docs say I should. Rendering this code in a page:

<p>{{ site.categories | inspect }}</p>

I get this (formatted by me for more clarity):

{"Misc"=>
	[
		#<Jekyll::Document _posts/2015-02-17-burningman-solar-power.html collection=posts>,
		#<Jekyll::Document _posts/2014-03-10-california-requirements-for-wiring-an-oven.html collection=posts>,
		#<Jekyll::Document _posts/2014-01-11-leadworth.html collection=posts>,
		…
	],
"Apple"=>
	[
		#<Jekyll::Document _posts/2024-02-09-AVP-Blender-Workflow.md collection=posts>,
		#<Jekyll::Document _posts/2024-02-02-SwiftUI-Menu-Commands.md collection=posts>,
		…
	],
	…

This is not an “array with two items, where the first item is the name of the tag and the second item being an array of posts with that tag.” as described in the docs.

However, if I render

<p>{{ site.categories | sort | inspect }}</p>

I get

[
	[
		"Apple",
		[#<Jekyll::Document _posts/2024-02-09-AVP-Blender-Workflow.md collection=posts>, #<Jekyll::Document _posts/2024-02-02-SwiftUI-Menu-Commands.md collection=posts>, …]
	],
	[
		"Misc",
		[#<Jekyll::Document…]
	],
	…
]

Which is an array of arrays of two items. And it’s sorted, unfortunately not case-insensitively. Using sort_natural, I get different output:

[{"Misc"=>[#<Jekyll::Document _p …

Which is basically the unsorted output stuck in an array of one item.

I want to list my categories, sorted naturally. I can’t find a way to do this very reasonable thing.

I am a little confused by your questions, so I will try to give you more information than you need, and I hope you will understand by the end :slight_smile:

Display the list of categories on a page or post

If you want to display the categories on a page or post. Here is the code to do that:

Ouptut the categories on the page or post

index.markdown

---
layout: home
title: Home
categories: [zed, alpha, delta, crater]
---
categories: {{page.categories}}

Output:
categories: zedalphadeltacrater

Sort and then output the categories on the page or post

index.markdown

---
layout: home
title: Home
categories: [zed, alpha, delta, crater]
---
Sorted categories:
{% assign sortedCategories = page.categories | sort %}
{%- for eachCategory in sortedCategories -%}
    {{eachCategory}},
{%- endfor -%}

Output:

Sorted categories: abc,alpha,crater,def,delta,some more,zed,

Sort the post or page categories and then do something with them

Here, we can use a for loop to add a comma to the end of each category in the list of categories in a page or post:

---
layout: home
title: Home
categories: [zed, alpha, delta, crater]
---
Categories separated by commas:&nbsp;
{%- for eachCategory in sortedCategories -%}
    {%- if forloop.last -%}
        {{eachCategory}}
    {%- else -%}
        {{eachCategory}},&nbsp;
    {%- endif -%}
{%- endfor -%}

Output:
Categories separated by commas: alpha, crater, delta, zed

Working with site categories

My understanding is site.categories is a hash where each key is a category name, and the value is an array of posts in that category. In other words, it is much more complicated than an array of two values.

Also, it is important to note that site.categories and site.tags were designed for posts, not pages.

For example, on my site, I have two post categories of:

Posts
agile-in-action-podcast, tech-blog

However, for your question, I added some more categories to index.markdown, which look like this:

Pages:
categories: [zed, alpha, delta, crater, abc, def, some more]

Get a list of site.categories for posts

Take this code as an example. Notice I am telling Jekyll I want a comma-separated list of all categories from site.categories :

{% assign postCategories = "" %}
{% for category in site.categories %}
  {% if forloop.last %}
    {% assign postCategories = postCategories | append: category[0] %}
  {% else %}
    {% assign postCategories = postCategories | append: category[0] | append: ", " %}
  {% endif %}
{% endfor %}
{{ postCategories }}

However, the output looks like this:

agile-in-action-podcast, tech-blog

So what gives? Where are the other categories on pages? If you want those, you need to iterate through pages.

Get all categories in posts and pages

Here is code that iterates through all posts and pages to get all the categories. You can add whatever code you want, but this will create a comma-separated list of all of them.

:warning: Warning: This is an expensive piece of code because it will iterate through the entire collection of pages and posts. Granted, Jekyll does some caching, but remember that if your site gets large, this is not an ideal approach.

:arrow_right: Tags and categories were defined for posts, not pages. However, they can be added to pages, it is simply not part of the Jekyll collection. Therefore, the following code is a hack.

index.html
{% comment %} Get all post categories {% endcomment %}
{% for post in site.posts %}
  {% for category in post.categories %}
    {% unless siteCategories contains category %}
      {% if siteCategories != "" %}
        {% assign siteCategories = siteCategories | append: ", " %}
      {% endif %}
      {% assign siteCategories = siteCategories | append: category %}
    {% endunless %}
  {% endfor %}
{% endfor %}

{% comment %} Get all page categories and append to a list of site categories {% endcomment %}
{% for page in site.pages %}
  {% if page.categories %}
    {% for category in page.categories %}
      {% unless siteCategories contains category %}
        {% if siteCategories != "" %}
          {% assign siteCategories = siteCategories | append: ", " %}
        {% endif %}
        {% assign siteCategories = siteCategories | append: category %}
      {% endunless %}
    {% endfor %}
  {% endif %}
{% endfor %}

Site categories: {{ siteCategories }}

Output from my site:

Site categories: agile-in-action-podcast, tech-blog, zed, alpha, delta, crater, abc, def, some more

Conclusion

If you use the official site.categories, it is designed for posts (blog posts), not pages (like index.html), but you can approximate the use so your code looks and feels the same.

If you iterate through site.categories, you can display them and work with each individually. I put a comma next to each category in my examples, but I could have easily used markdown or HTML to turn them into URLs, lists, tables, etc.

Jekyll requires an understanding of your intent, so make sure you prefix categories with page or site, like this: page.categories or site.categories

I hope this helps!

So this doesn’t work properly if you have mixed capitalization (e.g. “Apple” and “iOS”), because sort does a case-sensitive sort. And I need site.categories, not page.categories.

There is a similar example for the tags to list the category keys:

{% for i in site.categories %}
  {{ i[0] }}
{% endfor %}

To apply a natural sort to the categories using the sort_natural filter:

{% assign list = '' %}
{% for i in site.categories %}
  {% assign list = list | append: i[0] | append: ','  %}
{% endfor %}

{{ list | split: ',' | sort_natural | join: ',' }}
1 Like

Ah, thank you. The {% assign list = list | append: i[0] | append: ',' %} will make it work, I think. Clunky, but should do the job.

Pity sort_natural doesn’t behave the same as sort.

If you want a more advanced option you can build a filter by yourself: Filters | Jekyll • Simple, blog-aware, static sites

Yeah, that seems like a much more elegant solution, thank you.

Also, it prompted me to look into Liquid’s sort and sort_natural. They are identical in implementation, but I can’t, for the life of me, figure out why I get completely different results with sort_natural. As in, the structure of the data returned is different, not just the sort order.