LoginSignup
1
0

More than 3 years have passed since last update.

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

Posted at

目標

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

これを

before image

こうする。

after image

実行環境

  • 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へ)。

実装内容

解説は後ろで行う。

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__indextaxonomy__countクラスは実装済みなのでここでは省略)。

.taxonomy__subindex { // 親タグのクラスtaxonomy__indexで細かい指定はしているからこれで十分と判断
  list-style: none;
}

解説

解説1:親子カテゴリーを要素に持つ配列を作成

{% assign my_posts = site.posts | group_by: 'categories' %}とすることで、「カテゴリー別にアイテムをソートし、それらをカテゴリ名の下にグループ化」1できる。なお、今回の場合はroot_directory/_postscategoriesを登録している記事ファイルが保存されているため、参考資料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.last3を使って、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_namesplitで分割して、[親子ペア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でそのカテゴリーを持つ記事数が参照できる。そこで、親子カテゴリーを出力する際にforifを使って当該categoryを見つけ出し、category[1].sizeをカテゴリー名の横に出力させる。また、見つけ次第breakforループは抜ける。

親カテゴリー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を使えば文字列で問題ない

参考資料

脚注は以下↓

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0