给 Jeklly 博客添加 API,然后用 python 读取 API 中的 tag 信息,按固定格式生成 Tag Page。

1. Introduction

在这里,我们将一步一步介绍 Jeklly 生成 Tag Page 的过程,也能管中窥豹,深入理解 Jeklly 由 markdown 文件生成静态网页的原理。

1.1. 给 posts 添加 tags

首先,当我们在 markdown 文档中书写时,要输入头文件信息 YAML front matter,以这篇博客为例,我们输入的 front matter 为:

---
layout: post
title: " Jeklly 生成 Tag Page"
date: 2021-07-23 21:00:00
tags: blog
---

其中 tags: blog 就表示我们给这篇文章设定的标签为 blog

1.2. 收集所有 posts 中的 tags

_includes 文件夹下的 tags.html 中(其实这样起名并不合适,因为根目录下还有一个同名的tags.html 文件,两者容易混淆),有如下 Liquid 代码:

{% assign rawtags = "" %}
{% for post in site.posts %}
  {% assign ttags = post.tags | join:'|' | append:'|' %}
  {% assign rawtags = rawtags | append:ttags %}
{% endfor %}
{% assign rawtags = rawtags | split:'|' | sort %}

{% assign site.tags = "" %}
{% for tag in rawtags %}
  {% if tag != "" %}
    {% if tags == "" %}
      {% assign tags = tag | split:'|' %}
    {% endif %}
    {% unless tags contains tag %}
      {% assign tags = tags | join:'|' | append:'|' | append:tag | split:'|' %}
    {% endunless %}
  {% endif %}
{% endfor %}

其大概意思是遍历所有发布的页面 site.posts ,然后将所有页面的 tag 都汇入 rawtags 中,然后对 rawtags 分割后排序,这样就得到了一个有序的 tag 数组。然后将数组去重合并,就得到了所有 tag 的汇总 tags

1.3. 生成 site.tags

我一直没搞懂 site.tags 是怎么生成的,上面的代码一顿操作猛如虎,但仔细一看只是作用在 tags 上,跟 site.tags 完全无关。但其他代码中也没找到相关操作,非常神奇。

但不管怎么样,在一些不明觉厉的操作下, site.tags 被搞出来了。

这个博客当前的 site.tags 长这样:

{
  “blog”=>[#<Jekyll::Document _posts/2021-7-21-introduction.md collection=posts>], 
  “3D”=>[
    #<Jekyll::Document _posts/2021-7-22-pac.md collection=posts>,
    #<Jekyll::Document _posts/2021-7-22-3D-rotation.md collection=posts>],
  “ML”=>[#<Jekyll::Document _posts/2021-7-22-pac.md collection=posts>],
  “test”=>[#<Jekyll::Document _posts/2021-7-22-pac.md collection=posts>]
}

首先是 tag 的名称,每个 tag 都对应一个数组,里面包含了所有与该 tag 相关的 posts。

我们可以用 tag[0] 提取该 tag 的名称,用 tag[1] 提取包含该 tag 的 posts 。

1.4. 显示当前 post 的tags

我们想将当前 page 的所有 tag 都显示出来,效果如下图。

image-20210725011927163

那么可以通过以下代码来实现,并且点击 tag 链接,就能跳转到对应的 tag page,其相对网址在 /tag/tag_name ,例如 ML 的跳转链接为 https://theigrams.github.io/zjblog/tag/ML

<span>[
  {% for tag in page.tags %}
    {% capture tag_name %}{{ tag }}{% endcapture %}
    <a href="/tag/{{ tag_name }}">
        <code class="highligher-rouge"><nobr>{{ tag_name }}</nobr></code>
        &nbsp;
    </a>
  {% endfor %}
]</span>

2. 生成 Tag Page

接下来就进入了我们的重头戏部分,我们要给每个 tag 都生成一个对应的网页,该网页中包含了所有与该 tag 相关的文章,大致的预期效果如下:

image-20210725012701061

这对于一般的网站来说是小 case,但我们这个 Jeklly 是托管在 GitHub Page 上的静态网站,不能说临时执行一个脚本来生成网页。

2.1. 解决方案

不过办法还是有的,上网搜了一圈,主要的解决方案分为 2 种:

方案一:通过插件解决,但缺点是 GitHub 不支持这类插件,要么是定义一系列 GitHub Action 来自动化操作,或者在本地生成网页文件,然后推送到 gh 分支部署。

  1. How do I tag posts in Jekyll? Jekyll tagging made simple. (untangled.dev)
  2. Jekyll 添加 tag 专属页面 | TaoAlpha’s Blog
  3. 在 Github Pages 中使用Jekyll插件 | TaoAlpha’s Blog

方案二:生成 tag.mdtag.html ,然后套用 tagpage.html 模板进行显示。

tagpage.html 代码如下:

---
layout: default
---

<div class="post other-pages">
    <h1>Tag: {{ page.tag }}</h1>
    <ul>
        {% for post in site.tags[page.tag] %}
        <li><a href="{{ site.baseurl }}{{ post.url }}">{{ post.title }}</a> ({{ post.date | date_to_string }})<br> {{ post.description }}
        </li>
        {% endfor %}
    </ul>
</div>
<hr>

tag.md 格式如下:

---
layout: tagpage
title: "Tag: ML"
tag: ML
---

可以看到,这个方案比较简单,甚至手动写一个 md 也行,但考虑到 tag 可能会有修改,那样的话就很麻烦了,所以还是通过代码自动化生成比较便捷。

现在的话,只需给定 tag 的名称,我们就能轻松写出一个 python 程序来生成 tag.md 文件。

那么问题在于,我们如何确定需要添加的 tag 呢?网上大部分的方案是搜索当前发布的文章,然后从中提取出 tag ,最后再比较确定出需要补充的 tag.md 文件。这个方案行得通,但不够优雅,更好的方法是我们可以构建一个 API,然后直接让 python 读取 API 来确定需要的 tag。

2.2. 为 Jeklly 添加 API

Jekyll 是静态博客生成器,只能生成静态页面,原则上是没法创建一个动态的 API。但灵活利用 Liquid 的语法,可生成一个包含 JSON 数据的文件,这就创建了一个简单的静态 API。

我们新建一个 api 文件夹,然后通过如下 Liquid 代码即可得到一个关于文章信息的 API。

---
layout: null
permalink: /api/post.json
---
[
    {% for post in site.posts %}{
        "title": {{ post.title | jsonify }},
        "url": {{ post.url | jsonify }},
        "category": {{ post.category | jsonify }},
        "date": {{ post.date | jsonify }},
        "tags": {{ post.tags | jsonify }}
    }{% unless forloop.last %},{% endunless %}
    
    {% endfor %}
]

我们将该代码保存为api 文件夹下的 post.json 文件,那么就能访问该 API 了。

在浏览器中输入地址 https://theigrams.github.io/zjblog/api/post.json 即可看到相关信息。

或者用 curl 指令在终端输入也完全 OK 👌。

image-20210725022724002

有了之前的知识,我们可以很轻松地写出关于 tags 的API:

---
layout: null
permalink: /api/tags.json
---
{
    {% for tag in site.tags %}
    {{tag[0] | jsonify}}: {{tag[1].size | jsonify}}{% unless forloop.last %},{% endunless %}
    {% endfor %}
}

该 API 可以通过 https://theigrams.github.io/zjblog/api/tags.json 访问。

2.3. Python 读取 API 并生成 tag.md 文件

import requests
import os

tags_get = requests.get('http://localhost:4000/zjblog/api/tags.json')
tags = tags_get.json()

# for tag in tags:
#     print(tag+':',tags[tag])

build_tags = [tag for tag in tags]

for root, dirs, files in os.walk('./tag'):
    for file_name in files:
        tag = file_name[:-3]
        if tag in tags and tag in build_tags:
            build_tags.remove(tag)
print("Building new tags:", build_tags)

tag_pattern = '---\n'
tag_pattern += 'layout: tagpage\n'
tag_pattern += 'title: "Tag: %s"\n'
tag_pattern += 'tag: %s\n'
tag_pattern += '---\n'

for tag in build_tags:
    with open('./tag/'+tag+'.md', 'w') as f:
        f.write(tag_pattern % (tag, tag))

代码非常简单,就不赘述了,在需要时运行一下即可。

3. References

[1] Shopify, “Liquid 模板语言中文文档,” Liquid 模板语言中文文档.

[2] “给GitHub Pages上的Jekyll站点添加标签支持,” Zero[Z.W.T] Blog, Aug. 28, 2017.

[3] L. Qian, “Jekyll Tags on Github Pages,” Long Qian, Feb. 09, 2017.

[4] “为 Jekyll 添加一个简单的 API | Fooleap’s Blog.”

[5] “Jekyll • 简单静态博客网站生成器,” Jekyll • 简单静态博客网站生成器.