Jekyllで記事数付き親子カテゴリー一覧表示

## 目標

Jekyllで各記事にカテゴリーを2個登録している場合、つまり親子カテゴリーを前提としている場合、親に対して入れ子構造で子を表示する。

これを

![before image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276577/44000c63-0fa3-da88-451f-7716b709dbea.png)

こうする。

![after image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276577/919aea54-4d12-5276-affc-9dbef7ab22d0.png)

## 実行環境

- 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](https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/)を採用

### 前提条件

ポストにカテゴリーを`categories`で必ず2個（親と子）登録していること。1個だけ、3個以上は対象外。

以下のようにポストのFront matterに登録する。

```md
---
categories:
  - Parent_category
  - Child_category
---
```

`root_directory/_pages/category_archive.md`のFront matterに`layout: categories`が登録されており、`root_directory/_layouts/categories.html`でカテゴリー一覧の表示内容が制御されている（詳細は[Minimal Mistakes](https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/)のGitHubへ）。

## 実装内容

解説は後ろで行う。

```html:root_directory/_layouts/categories.html
{% 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`クラスは実装済みなのでここでは省略）。

```css
.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`の中身は次のようになる。

```html
{“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`末尾に要素区切りの目安`|`が追加されないようにしている。

```html
{% 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`を使えば被っている親カテゴリー要素は削除できるが、親子の組み合わせが判別できなくなるのでなし）。

```html
{% 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](#注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](#注2)）。

[実装内容](#実装内容)のうち、今回の解説部分に絞って記述する。なお、冒頭の`finished_categories`定義は、定義されずに最初の`unless`判定に入るのが気持ち悪いため空配列で定義しているだけである。

```html
{% 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]。つまり、次のように出力したい。

```html
<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](#解説2親子分けして親の重複がない出力)実装コードの間にHTMLタグを挿入する。その際、各親カテゴリーの`</li>`タグは、次の親カテゴリーリストを作成する直前として、`{% unless finished_categories contains pair_categories[0] %}`の中に挿入する。また、最初の親カテゴリーリストを作成する直前は不要なので、`unless forloop.first`を追加する。

クラスは[実装内容](#実装内容)、およびJekyllテーマの[Minimal Mistakes](https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/)を参照する。

```html
{% 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タグを追加している。

```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](https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/)を参照する。

## まとめ

Jekyllにおいて、カテゴリー一覧を見やすくするために、親子カテゴリーがあること前提の「親+複数の子」で、入れ子構造表示されるようにした。個人的にはHTMLタグが一部前後した見た目の実装になっているためあまり気に入らないが、ひとまずは完了。

## 注釈

### 注1

`capture`を使わず、`map`でも可能（`remove: "\"`がなぜか必要）

```html
{% 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`を使えば文字列で問題ない

## 参考資料

- [Variable – Liquid template language](https://shopify.github.io/liquid/tags/variable/)
- [Array filters](https://shopify.dev/docs/themes/liquid/reference/filters/array-filters#map)
- [変数 | Jekyll • シンプルで、ブログのような、静的サイト](http://jekyllrb-ja.github.io/docs/variables/)

脚注は以下↓

[^1]: [ナビゲーション | Jekyll • シンプルで、ブログのような、静的サイト シナリオ8：front matterのプロパティに基づくアイテムの取得](http://jekyllrb-ja.github.io/tutorials/navigation/#%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA8front-matter%E3%81%AE%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AB%E5%9F%BA%E3%81%A5%E3%81%8F%E3%82%A2%E3%82%A4%E3%83%86%E3%83%A0%E3%81%AE%E5%8F%96%E5%BE%97)
[^2]: [Liquidで配列を作る、要素を追加する - Qiita](https://qiita.com/momonoki1990/items/13f97a23436c142e4c9a)
[^3]: [The forloop object](https://shopify.dev/docs/themes/liquid/reference/objects/for-loops#forloop-first)
[^4]: [Jekyll対応マークダウン記法チートシート - Qiita](https://qiita.com/mt_west/items/7a4f41c749ed582330e9#%E5%A4%89%E6%95%B0%E5%AE%9A%E7%BE%A9)
[^5]: [ul、liを使って要素を入れ子にし、各リストごとに別の装飾をする方法 | しろいぽんず](https://shiroi-ponzu.com/html-css/howto_ul-li/)
[^6]: [Liquidフィルタ | Jekyll • シンプルで、ブログのような、静的サイト](http://jekyllrb-ja.github.io/docs/liquid/filters/)