目標
Jekyllで各記事にカテゴリーを2個登録している場合、つまり親子カテゴリーを前提としている場合、親に対して入れ子構造で子を表示する。
これを
こうする。
実行環境
- Windows 10
- Jekyll 4.2.0
- ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x64-mingw32]
- Bundler version 2.2.2
- JekyllテーマとしてMinimal Mistakesを採用
前提条件
ポストにカテゴリーをcategories
で必ず2個(親と子)登録していること。1個だけ、3個以上は対象外。
以下のようにポストのFront matterに登録する。
---
categories:
- Parent_category
- Child_category
---
root_directory/_pages/category_archive.md
のFront matterにlayout: categories
が登録されており、root_directory/_layouts/categories.html
でカテゴリー一覧の表示内容が制御されている(詳細はMinimal MistakesのGitHubへ)。
実装内容
解説は後ろで行う。
{% comment %}
<!-- below is limited only in the case with all posts having a pair of (parent and child) categories -->
{% endcomment %}
<!-- 解説1 -->
{% capture posts_name %}{% assign my_posts = site.posts | group_by: 'categories' %}{% for my_post in my_posts %}{% assign post_name = my_post.name | remove: "[" | remove: "]" | remove: '"' %}{{ post_name }}{% unless forloop.last %}|{% endunless %}{% endfor %}{% endcapture %}
{% assign integrated_categories_names = posts_name | split: "|" | sort %}
{% assign finished_categories = "nil, nil" | split: ", " %} <!-- 解説2 -->
<ul class="taxonomy__index"> <!-- 解説3 -->
{% for integrated_categories_name in integrated_categories_names %}
{% assign pair_categories = integrated_categories_name | split: ", " %}
{% unless finished_categories contains pair_categories[0] %}
{% unless forloop.first %}
</li>
{% endunless %}
<li>
{% for category in site.categories %} <!-- 解説4 -->
{% if pair_categories[0] == category[0] %}
<a href="#{{ category[0] | slugify }}">
<strong>{{ category[0] }}</strong> <span class="taxonomy__count">{{ category[1].size }}</span>
</a>
{% break %}
{% endif %}
{% endfor %}
{% assign finished_categories = finished_categories | concat: pair_categories %}
{% endunless %}
<ul class="taxonomy__subindex">
<li>
{% for category in site.categories %}
{% if pair_categories[1] == category[0] %}
<a href="#{{ category[0] | slugify }}">
<strong>{{ category[0] }}</strong> <span class="taxonomy__count">{{ category[1].size }}</span>
</a>
{% break %}
{% endif %}
{% endfor %}
</li>
</ul>
{% endfor %}
</ul>
上に加えて、CSSファイルに以下を追加した(taxonomy__index
、taxonomy__count
クラスは実装済みなのでここでは省略)。
.taxonomy__subindex { // 親タグのクラスtaxonomy__indexで細かい指定はしているからこれで十分と判断
list-style: none;
}
解説
解説1:親子カテゴリーを要素に持つ配列を作成
{% assign my_posts = site.posts | group_by: 'categories' %}
とすることで、「カテゴリー別にアイテムをソートし、それらをカテゴリ名の下にグループ化」1できる。なお、今回の場合はroot_directory/_posts
にcategories
を登録している記事ファイルが保存されているため、参考資料1とは異なり.posts
としている。my_posts
の中身は次のようになる。
{“name”=>”["System", "Programming"]”, “items”=>[#<略>, 略], “size”=>3}
{“name”=>”["Others", "Diary"]”, “items”=>[#<略>, 略], “size”=>6}
{“name”=>”["Others", "English"]”, “items”=>[#<略>, 略], “size”=>1}
{“name”=>”["Entertainment", "Anime"]”, “items”=>[#<略>, 略], “size”=>3}
略
“name”
に親子カテゴリー、“items”
に該当記事情報、“size”
に記事数が定義されており、“name”
に重複はない。たとえば{{ my_posts[0].size }}
は3
である。
このままだと使えないので、my_posts
から“name”
の情報のみを抽出して配列化したいが、Liquidでは文字列を分割して要素を作成しなければ配列を定義できない2。
そこでまずはcapture
と要素区切りの目印として|
を加えた文字列posts_name
を定義する。同時に、“name”
には余計な装飾文字が含まれているのでremove
で削除する。なお、unless forloop.last
3を使って、posts_name
末尾に要素区切りの目安|
が追加されないようにしている。
{% capture posts_name %}{% assign my_posts = site.posts | group_by: 'categories' %}{% for my_post in my_posts %}{% assign post_name = my_post.name | remove: "[" | remove: "]" | remove: '"' %}{{ post_name }}{% unless forloop.last %}|{% endunless %}{% endfor %}{% endcapture %}
ここで一応の注意として、capture
を使う場合は余計な改行やインデントを入れて可読性を高めようとしてはならない。意図せぬ定義となってしまう4(これに気付くまで超時間悩まされた)。
上記で定義したposts_name
をsplit
で分割して、[親子ペア1, 親子ペア2, ...]
となる配列を作成する。この時点ではまだ親子ペアは1つの文字列になっているので、sort
することでアルファベット順に親子ペア要素を並べられる(親と子を切り分けて、uniq
を使えば被っている親カテゴリー要素は削除できるが、親子の組み合わせが判別できなくなるのでなし)。
{% assign integrated_categories_names = posts_name | split: "|" | sort %}
結果はたとえばintegrated_categories_names = ["Entertainment, Anime", "Others, Diary", "Others, English", "System, Programming"]
のように、「親1, 子1」, 「親2, 子2」, 「親3, 子3」
となる(注1)。
解説2:親子分けして親の重複がない出力
integrated_categories_names
を各要素(for pair_categories in integrated_categories_names
)でsplit: ", "
すれば、親子それぞれを要素に持つ配列pair_categories
を定義できる(pair_categories[0]
に親1、pair_categories[1]
に子1、次の要素ではpair_categories[0]
に親2、pair_categories[1]
に子2、以下続く)。
その上で親カテゴリーには重複があるため、「同じ親なら子のみ出力」させる必要がある。そこでfinished_categories
配列を用意することで、親カテゴリーを出力するたびにpair_categories
の要素をfinished_categories
に追加(concat
)して、if
文で「次のpair_categories[0]
=親カテゴリーがfinished_categories
に含まれているか否か」を実装する(注2)。
実装内容のうち、今回の解説部分に絞って記述する。なお、冒頭のfinished_categories
定義は、定義されずに最初のunless
判定に入るのが気持ち悪いため空配列で定義しているだけである。
{% assign finished_categories = "nil, nil" | split: ", " %}
{% for integrated_categories_name in integrated_categories_names %}
{% assign pair_categories = integrated_categories_name | split: ", " %}
{% unless finished_categories contains pair_categories[0] %}
{{ pair_categories[0] }}
{% assign finished_categories = finished_categories | concat: pair_categories %}
{% endunless %}
{{ pair_categories[1] }}
{% endfor %}
解説3:HTML出力
親子要素を入れ子にしたリストをHTMLで出力する5。つまり、次のように出力したい。
<ul>
<li>
親カテゴリー1
<ul>
<li>
子カテゴリー1-1
</li>
<li>
子カテゴリー1-2
</li>
</ul>
</li>
<li>
親カテゴリー2
<ul>
<li>
子カテゴリー1-2
</li>
<li>
子カテゴリー2-2
</li>
</ul>
</li>
</ul>
解説2実装コードの間にHTMLタグを挿入する。その際、各親カテゴリーの</li>
タグは、次の親カテゴリーリストを作成する直前として、{% unless finished_categories contains pair_categories[0] %}
の中に挿入する。また、最初の親カテゴリーリストを作成する直前は不要なので、unless forloop.first
を追加する。
クラスは実装内容、およびJekyllテーマのMinimal Mistakesを参照する。
{% assign finished_categories = "nil, nil" | split: ", " %}
<ul class="taxonomy__index">
{% for integrated_categories_name in integrated_categories_names %}
{% assign pair_categories = integrated_categories_name | split: ", " %}
{% unless finished_categories contains pair_categories[0] %}
{% unless forloop.first %}
</li>
{% endunless %}
<li>
{{ pair_categories[0] }}
{% assign finished_categories = finished_categories | concat: pair_categories %}
{% endunless %}
<ul class="taxonomy__subindex">
<li>
{{ pair_categories[1] }}
</li>
</ul>
{% endfor %}
</ul>
汚くなってきた <ul class="taxonomy__subindex">...</ul>
がすべての子カテゴリーで出力されるが、問題はないのでそのままにしている(解消しようとすると可読性が下がる)。
解説4:各カテゴリーの記事数を表示
親子の区別なく、全カテゴリー情報はsite.categories
の各要素をcategory
とすると、category[0]
でカテゴリー名、category[1].size
でそのカテゴリーを持つ記事数が参照できる。そこで、親子カテゴリーを出力する際にfor
とif
を使って当該category
を見つけ出し、category[1].size
をカテゴリー名の横に出力させる。また、見つけ次第break
でfor
ループは抜ける。
親カテゴリーpair_categories[0]
を出力させるコードを抜粋すると以下のようになる。なお、slugify
で小文字のURL化6をして<a>
タグ出力をする等、一部HTMLタグを追加している。
{% for category in site.categories %}
{% if pair_categories[0] == category[0] %}
<a href="#{{ category[0] | slugify }}">
<strong>{{ category[0] }}</strong> <span class="taxonomy__count">{{ category[1].size }}</span>
</a>
{% break %}
{% endif %}
{% endfor %}
クラスは実装内容、およびJekyllテーマのMinimal Mistakesを参照する。
まとめ
Jekyllにおいて、カテゴリー一覧を見やすくするために、親子カテゴリーがあること前提の「親+複数の子」で、入れ子構造表示されるようにした。個人的にはHTMLタグが一部前後した見た目の実装になっているためあまり気に入らないが、ひとまずは完了。
注釈
注1
capture
を使わず、map
でも可能(remove: "\"
がなぜか必要)
{% assign my_posts = site.posts | group_by: 'categories' %}
{% assign posts_names = my_posts | map: "name" | sort %}
{% assign integrated_categories_name = posts_names | remove: "[" | remove: "]" | remove: '"' | remove: "\" %}
注2
今考えると、別に配列でなくともappend
を使えば文字列で問題ない
参考資料
脚注は以下↓