Jekyll 的 Categories 设计
当博客开发进行到 Category 部分时,很多头疼的问题接踵而来:
- 类目如何分层,分多少层?
- 顶级分类能不能允许文章与子分类同存?
- 如何实现方便快捷的交互 UI?
思考的过程中仔细参阅了 Jekyll Docs,还有 Google 上一些关于 WordPress 的分类规则文章,以及 Evernote 的产品设计,最后做出以下设计。
设计目标
- 出于归类精简的初衷,类目最多分两级。
- 一级分类下,既可展示子分类,也有入口可直接展示所有文章。
- 博客顶栏
Categories
模块展示所有顶级分类及其各自的子类(文章列表隐藏)。
交互
点击分类名称进入新页面,展示所有该分类下的文章列表。一级分类则显示根目录文章以及各个子分类下的所有文章。
实现细节
下文实现环境为 Bootstrap 3, 图标为 Fontawesome 系列。
Front matter 约定
在撰写 Post 时,分类定义可用 category
或 categories
定义,但是必须严格遵守 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> {{ page.title }}
<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 }} </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
目录下的所有文章的 category
或 categories
,分别为每个获取的分类名建立一个同名的.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>
<a class="panel-title" href="/category/{{ category_name }}.html">{{ category_name }}</a>
{% 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> <a href="/category/{{ sub_category }}.html">{{ sub_category }}</a>
{% assign posts_size = site.categories[sub_category] | size %}
<span class="text-muted small"> {{ 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 板块体验。