Post

Jekyll 的 Categories 设计

当博客开发进行到 Category 部分时,很多头疼的问题接踵而来:

  1. 类目如何分层,分多少层?
  2. 顶级分类能不能允许文章与子分类同存?
  3. 如何实现方便快捷的交互 UI?

思考的过程中仔细参阅了 Jekyll Docs,还有 Google 上一些关于 WordPress 的分类规则文章,以及 Evernote 的产品设计,最后做出以下设计。

设计目标

  1. 出于归类精简的初衷,类目最多分两级。
  2. 一级分类下,既可展示子分类,也有入口可直接展示所有文章。
  3. 博客顶栏 Categories 模块展示所有顶级分类及其各自的子类(文章列表隐藏)。

交互

点击分类名称进入新页面,展示所有该分类下的文章列表。一级分类则显示根目录文章以及各个子分类下的所有文章。

实现细节

下文实现环境为 Bootstrap 3, 图标为 Fontawesome 系列。

Front matter 约定

在撰写 Post 时,分类定义可用 categorycategories 定义,但是必须严格遵守 YAML 语法。

单个分类用 category 声明

e.g.

1
2
3
category: live   # 正确!

category: [live, food] # 错误

一个以上分类用 categories 声明,其值为 YAML 数组,但是元素数量不能超过 2 个。

e.g.

1
2
3
4
5
categories: live # 允许,但是不建议

categories: [live, food] # 正确

categories: [live, food, drink] # 错误,分类层数超过 2

定义 Category 样式

在目录 _layout 中新建文件 category.html,添加源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
layout: default
---

<row>
  <div class="col-sm-12">
    <h1>
      <i class="far fa-folder-open"></i>&nbsp;{{ page.title }}&nbsp;&nbsp;
      <span class="badge">{{ site.categories[page.category] | size }}</span>
    </h1>
    <ul>
      {% for post in site.categories[page.category] %}
      <li>
        <h5>
          <span class="text-muted small"
            >{{ post.date | date_to_string }}&nbsp;&nbsp;</span
          >
          <a href="{{ post.url | absolute_url }}">{{ post.title }}</a>
        </h5>
      </li>
      {% endfor %}
    </ul>
  </div>
</row>

生成每个 Category 页

为了实现每个页面展示某个分类下的 Post 列表,需要为每个分类实现一个独立的静态页面。在 Jekyll 工程根目录创建一个文件夹 category,然后在其内以每个分类的名字命名,依次创建新的 HTML 文件。例如对于分类 live,则页面命名为 live.html,内容为:

1
2
3
4
5
---
layout: category
title: Live
category: live
---

在建站初期,文章数量较少时这种手动方式还是可以实施的。但是,当文章数量暴涨的时候,这种冗余的操作就显得很费时费力了,所以可以用 Python 脚本完成这项工作。

自动化脚本

页面创建的 Python 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import os
import glob
import yaml
import shutil


POSTS_PATH = '_posts'

CATEGORIES_PATH = 'category'
CATEGORY_LAYOUT = 'category'


def get_front_matter(path):
    end = False
    front_matter = ""
    with open(path, 'r') as f:
        for line in f.readlines():
            if line.strip() == '---':
                if end:
                    break
                else:
                    end = True
                    continue
            front_matter += line

    return front_matter


def get_categories():
    all_categories = []

    for file in glob.glob(os.path.join(POSTS_PATH, '*.md')):
        meta = yaml.load(get_front_matter(file))
        try:
            category = meta['category']
        except KeyError:
            try:
                categories = meta['categories']
            except KeyError:
                err_msg = (
                    "[Error] File:{} at least "
                    "have one category.").format(file)
                print(err_msg)
            else:
                if type(categories) == str:
                    error_msg = (
                        "[Error] File {} 'categories' type"
                        " can not be STR!").format(file)
                    raise Exception(error_msg)

                for ctg in meta['categories']:
                    if ctg not in all_categories:
                        all_categories.append(ctg)
        else:
            if type(category) == list:
                err_msg = (
                    "[Error] File {} 'category' type"
                    " can not be LIST!").format(file)
                raise Exception(err_msg)

            if category not in all_categories:
                all_categories.append(category)

    return all_categories


def generate_category_pages():
    categories = get_categories()
    if os.path.exists(CATEGORIES_PATH):
        shutil.rmtree(CATEGORIES_PATH)

    os.makedirs(CATEGORIES_PATH)

    for category in categories:
        new_page = CATEGORIES_PATH + '/' + category + '.html'
        with open(new_page, 'w+') as html:
            html.write("---\n")
            html.write("layout: {}\n".format(CATEGORY_LAYOUT))
            html.write("title: {}\n".format(category.title()))
            html.write("category: {}\n".format(category))
            html.write("---")
            print("[INFO] Created page: " + new_page)
    print("[INFO] Succeed! {} category-pages created.\n"
          .format(len(categories)))


def main():
    generate_category_pages()

main()

Python 脚本的作用是搜集 _posts 目录下的所有文章的 categorycategories,分别为每个获取的分类名建立一个同名的.md定义文件,存放于 category 目录下,该目录为自动创建,每次运行脚本都会覆盖一遍。页面采用的样式是上一步定义的 _layout/category.html

每次新添/删除 Post,或者修改已有 Post 的 Categories 信息时,都应该运行一次上述脚本。

分类动态展开的实现

在 Categories 总览页面,实现折叠的效果:

二级分类页面展示实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{% assign sort_categories = site.categories | sort %}

{% for category in sort_categories %}
  {% assign category_name = category | first %}
  {% assign posts_of_category = category | last %}
  {% assign first_post = posts_of_category[0] %}

  {% if category_name == first_post.categories[0] %}
    {% assign sub_categories = "" %}
    {% for post in posts_of_category %}
      {% if post.categories.size > 1 %}
        {% assign sub_categories = sub_categories | append: post.categories[1] | append: "|" %}
      {% endif %}
    {% endfor %}

    {% assign sub_categories = sub_categories | split: "|" | uniq %}
    {% assign sub_categories_size = sub_categories | size %}

<div class="panel-group">
  <div class="panel panel-default">
    <div class="panel-heading" id="{{ category_name }}">
      <i class="far fa-folder"></i>&nbsp;
      <a class="panel-title" href="/category/{{ category_name }}.html">{{ category_name }}</a>&nbsp;
      {% assign top_posts_size = site.categories[category_name] | size %}
      <span class="text-muted small">
        {{ sub_categories_size }} folder{% if sub_categories_size > 1 %}s{% endif %}, {{ top_posts_size }} post{% if top_posts_size > 1 %}s{% endif %}
      </span>

      <a data-toggle="collapse" href="#grp_{{ category_name }}" class=" {% if sub_categories_size <= 0%}inactiveLink{% endif %}">
        <i class="fas fa-angle-down" style="float: right; padding-top: 0.5rem;"></i>
      </a>
    </div>

    {% if sub_categories_size > 0 %}
    <div id="grp_{{ category_name }}" class="panel-collapse collapse">
      <ul class="list-group">
        {% for sub_category in sub_categories %}
        <li class="list-group-item" style="padding-left: 4rem;">
          <i class="far fa-folder"></i>&nbsp;<a href="/category/{{ sub_category }}.html">{{ sub_category }}</a>
          {% assign posts_size = site.categories[sub_category] | size %}
          <span class="text-muted small">&nbsp;{{ posts_size }}</span>
        </li>
        {% endfor %}
      </ul>
    </div>
    {% endif %}
  </div>
</div>
  {% endif %}
{% endfor %}

考虑锦上添花,在顶类开合状态切换的同时,更改文件夹符号以及尖头符号的方向,jQuery 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$(function () {
  var prefix = "grp_";

  $(".collapse").on("hide.bs.collapse", function () {
    var id = $(this).attr("id").substring(prefix.length);
    if (id) {
      //兼容 bootstrap 首页顶栏手机尺寸缩放
      $("#" + id + " i.far.fa-folder-open").attr("class", "far fa-folder");
      $("#" + id + " i.fas.fa-angle-up").attr("class", "fas fa-angle-down");
    }
  });

  $(".collapse").on("show.bs.collapse", function () {
    var id = $(this).attr("id").substring(prefix.length);
    if (id) {
      //兼容 bootstrap 首页顶栏手机尺寸缩放
      $("#" + id + " i.far.fa-folder").attr("class", "far fa-folder-open");
      $("#" + id + " i.fas.fa-angle-down").attr("class", "fas fa-angle-up");
    }
  });
});

至此,所有工作完成,具体效果可跳到 Categories 板块体验。

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.