Hugo 静态博客建站记 - 自写主题
2019年6月22日 - 14044 字

很早就想建一个自己的博客了,每每看到别人的好看的博客就心里痒痒. 高中及以前写过一些回忆录都发表在 QQ 空间上,高中以来有意识地进行一些写作,觉得不错的都发表在新浪博客上,稍短的就写成长微博. 但有时想写一些专业性(数学)的东西,新浪博客的富文本编辑器就不够用了,而微博由于国内的审查机制不明确不规范也决定放弃,于是着手建博客.

构想

大一、大二的时候在学校一个学生组织的网络技术工作站做过两年的后端,那时构想的博客就是租一台VPS,用学到的技术 [Django (Python) + MySQL] 做一个超级棒的网站,想实现什么功能就实现什么功能. 后来一门心思学数学后发觉我时间有限,于是静态博客这种东西吸引了我,经过比较选择了 Hugo + GitHub Pages. 优点是我不用租 VPS 了,甚至不用申请域名,访问快速、安全,不用日常维护,简单、省时间.

初期的安装与结构规划

在网络上可以找到大量关于此的教程与文章,我在这里主要参考了官方的文档来安装.

之后粗略读了一下 Hugo 的内容管理,主要是第一部分内容组织(下面的章节3.1中提到的一篇文章也是对同样的内容进行了讲解),知道了它是完全按照文件夹的目录层次进行组织划分的,每一个目录叫做一个 section(对应一个 type) ,除此之外还可以进行自由地分类(taxonomy),比如常见的有 categories 【我的理解:以方便进行更细致的分类】、tags 【我的理解:以方便做更立体更全面可交叉的分类】和 series 【我的理解:以方便把过长的文章打散或聚合可以合并到同一个系列的文章】. 于是我便开始设计我的博客结构. 现在这个网站呈现的样子就恰恰是我当时设想的样子.

主题的制作

由于 Hugo 没有默认的主题,所以在安装的第三步需要到网络上寻找一个主题使用. 实际上,我找了很久很久,都没有找到称心如意的主题. 根据我对博客结构的设计,它应该像现在呈现的这样有多级 section 并保持按分级陈列在导航栏中固定住,而 categories、tags、series 的分类方法应当隐藏于某一个页面中. 而网络上大多数可以用作博客的主题的导航栏都是按 taxonomy 分的类,而非按 section 进行陈列,即便按 section 陈列也只有一级 section,不能满足我的需要;且外观上鲜有我喜欢的样式——唯一的例外来自名为 cupper 的主题,但以它为基础进行修改并不可取,尤其是我对 Hugo 还什么都不懂——在这样的情况下学习并去读别人的代码(我开始真的是这样做的!)令人烦躁. 于是我决定学习 Hugo,完完全全自己动手写一个我想要的主题(不过基本的外观还是借鉴了 cupper 这个主题,致谢!).

我的主题完全开源,您可以在这里阅读它的代码甚至使用.

学习 Hugo 是如何运作的

这里我主要参考了 Yash Agarwal, Develop a Theme for Hugo,这篇文章对内容管理同样做了非常细致的说明和区分. 可能是因为我会 Django,而 Hugo 处理网页的方式和 Django 类似1,我读起来非常顺利,也觉得非常简单. 但无论如何,这篇文章非常友好,通读下来就能对 Hugo 的运作方式有一个最基本的了解——比如主页 (index.html)、列表页 (list.html) 和文章页 (single.html) 这三个最基本的页面是分别做什么的,它们分别在什么地方,怎么在一个固定的 HTML 模板中写入不同的内容,逻辑运算以什么方式写进去,静态文件比如 CSS、JS 以及图片等可提供下载的文件放在哪里以及如何引用,等等. 我按照它的步骤完全进行了一遍,这也就标志着我制作我自己的主题正式开始了.

具体的过程也不多说,只要配合着官方文档基本就能搞定. 这一步需要参考的更细致的内容主要是模板一章,不仅是前面提到的三个最基本页面,所有有可能用到的模板都在这一章介绍得很清楚,另外就是查找顺序非常非常重要,提前规划好可以最大程度地复用代码,减少工作量. 上面我参考的那篇文章中在第 3、4 小节给出的调试方法:删去/public文件夹,使用$ hugo server --watch --verbose命令,是非常有用的.

TOML、YAML 和 JSON 的学习

Hugo 的配置文件支持这三种格式. 尽管官方文档给出的例子大多数都是三种格式都有,但仅仅是官方的例子并不足够使用;又考虑到在进行检索时网络上的各位博主也是用什么的都有,且大多数博主记录自己的建站过程肯定不会像官方那样提供三个格式的配置;这三种格式的转化并不是显而易见地见几个例子就能触类旁通(比如 TOML 中的双中括号 [[something]] 就不是很容易猜到 ):因此对这三种格式进行学习是有必要的. 这里我主要参考了如下几篇文章:

  1. 龙腾道, TOML 教程;
  2. 龙腾道, TOML v0.5.0;
  3. 阮一峰, YAML 语言教程;
  4. 菜鸟教程, JSON 教程.

一个可以进行在线格式转换的网站也是非常非常有帮助的.

Hugo 的细节学习

Hugo 的模板部分当然要理解清楚(把需要用到的理解清楚即可),逻辑的部分更为重要一些. 通用的函数非常有价值,熟读后对细节的处理可以更高效.

其中最重要的是 where 函数,它在列表页 (list.html) 对文章的筛选和排序起着重要的作用.

另外,就像任何一门语言一样,确定好数据类型、确定好对象,针对不同的对象使用该对象拥有的方法是非常重要的,尤其是写了 {{ range ... }} 之后更要注意遍历的是什么类型的对象. 我这次在这一点上走了很多弯路,不注意对象是什么就把知道的方法乱用一气,把 menu 的方法和 page 的方法弄混了,看着报错信息怀疑人生,不知道出了什么错误. 所有的对象以及它们的参数/方法都可以在这里找到,至少以我的经验来看,Site、Page、Menu Entry 这三个是最最常用的,把这三种对象的参数/方法熟读后对怎么解决问题就会反应得比较快.

section 的陈列

这里选择的是使用 Menu Entry 来进行. 如果只有一级 section 的话,官方提供的懒人博客方法是简单有效的. 不过它对两级以上的 section 只能按第一级 section 建 menu:

…this enables you to creat a top-level menu…

且它不能默认支持多语言. 这样看来,在它的基础上手动补充一些信息比如手动添加低级 section 的 menu 以及手动添加其他语言的 menu 在我看来就不如不用这个懒人方法,所有 menu 全部手动创建.

正如官方文档所说,创建 menu 有两种方法:在配置文件里写或在多个 _index.md 的 front matter 里写. 我没在网络上找到合适的例子,所以在这里摸索了一段时间,我也是在这个时候才学习了前面小节中提到的三种配置文件的语法. 这里给出我个人摸索出的成果——多语言版本的、 YAML 格式配置文件的、创建多级别 section 的 Menu Entry 的代码:

(在这个问题上 gohugoio 的 hugo 官方项目的 issue#465 下的讨论尤其是 vjeantet 的评论中的例子给了我非常多的启发,这个例子因为部署在 GitHub 上而在四年后的今天依然是可以查看的,表示感谢!而这个特性直至四年后的今天依然没有被官方支持,表示谴责!)

目录结构:

$ tree -a content/
content/
|-- section1
|   |-- _index.md
|   |-- _index.en.md
|   |-- section11
|   |   |-- _index.md
|   |   '-- _index.en.md
|-- section2
|   |-- _index.md
'   '-- _index.en.md

方法 1——在配置文件里写:

 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
# config.yaml
defaultContentLanguage: zh # 默认语言是中文
defaultContentLanguageInSubdir: true # 所以下面的 url 里即便是默认的中文也带着/zh/
menu:
  main:
    - url: /zh/section1/ # 和目录名字一致
      identifier: s1 # 给下面的 children 确定 parent 用
      name: 第一部分 # 可以不同语言用不同的名字
      weight: 1 # 排序用
    - url: /zh/section2/
      identifier: s2
      name: 第二部分
      weight: 2
    - url: /zh/section1/section11/
      identifier: s11
      parent: s1 # parent 的 identifier
      name: 第一部分第一小节
      weight: 1
    
languages:
  zh:
    languageCode: zh-cn
    languageName: 简体中文
    title: 网站名
    weight: 1
  en:
    languageCode: en-us
    languageName: English
    title: Site's Name
    weight: 2
    menu:
      main:
        - url: /en/section1/
          identifier: s1
          name: SECTION I
          weight: 1
        - url: /en/section2/
          identifier: s2
          name: SECTION II
          weight: 2
        - url: /en/section1/section11/
          identifier: s11
          parent: s1
          name: SECTION I PART I
          weight: 1

方法 2——在 front matter 里写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# config.yaml
defaultContentLanguage: zh
defaultContentLanguageInSubdir: true
languages:
  zh:
    languageCode: zh-cn
    languageName: 简体中文
    title: 网站名
    weight: 1
  en:
    languageCode: en-us
    languageName: English
    title: Site's Name
    weight: 2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /content/section1/_index.md
---
menu:
  main:
    # 不用写 url 了
    identifier: s1
    name: 第一部分
    weight: 1
---
可写一些内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /content/section1/_index.en.md
---
menu:
  main:
    # 不用写明语言了
    identifier: s1
    name: SECTION I
    weight: 1
---
Optional content here
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /content/section1/section11/_index.md
---
menu:
  main:
      identifier: s11
      parent: s1
      name: 第一部分第一小节
      weight: 1
---
可写一些内容

因为我想做一个支持中英法三语的博客,而把 Menu Entry 的配置写在那么多分散的文件里显然是过于麻烦而又不好管理,所以一开始在这里我毫不犹豫地选择了方法 1——在配置文件中写. 然而,后来在做一级 section 的 list 的时候,我却找不到办法完成这件事,因为筛选文章、排序文章这种事都是 Page 对象的方法,确定 section 却是 Menu Entry 对象做的,然而在所有的 Menu Entry 方法中,只有 .Page 是唯一可以把 Menu Entry 对象转化为 Page 对象的方法——可是它上面赫然写着(至少截至到2019-06-25):

This will be non-nil if the menu entry is set via a page’s front-matter and not via the site config.

虽然也并不知道它为什么用“non-nil”这样的措辞,就好像得到空集才是官方期望的一样. 但不管怎样,我又把 Menu Entry 的配置全部换成了方法 2——在各个 _index.md 中写,倒是也不需要写明语言和写明 url 了.

数学写作的支持

MathJax 的布置与 Markdown 数学支持的了解

希望浏览器能支持渲染数学公式,我这里选择了 MathJax. 按照其官方的文档,在模板中添加一行代码即可完成.

我同时又希望能无缝接轨 LaTeX 文件,这样我既可以自己本地编译 PDF 格式的文档,又可以让它以同样漂亮的格式显示在网页上. 关于 Markdown 的支持,这里值得参考的文章是谢益辉, MathJax 与 Markdown 的究极融合. 不过由于数学环境在 Markdown 中不得不以代码块 `$math code$` 的样式用反引号将它包裹起来,而我需要的是把 LaTeX 代码直接拿过来就能用,不需要再进行额外的更改,因此继续寻找更好的工具.

我把 MathJax 换成了 KaTeX,因为后者无任何依赖、速度快、而且能自己部署. 这样一来,除了流量分析以及评论功能部分以外,本站完全没有使用任何其他的第三方 JavaScript 了!在本地预览的时候根本不需要联网,离线就能使用!KaTeX 的使用方法也很简单,按照其文档引入 CSS 和 JavaScript,然后我又加了两个插件,分别是自动渲染(auto-render)和复制 TeX 代码支持(copy-tex). 虽然比起 MathJax 它支持的 LaTeX 格式不够多,但对我来说写点简单的东西已经足够了,复杂的东西我就直接生成 PDF 了.

2021.11.02

配合 Pandoc 尝试 Markdown

听闻 Pandoc 是一个强大的工具,而 Hugo 是支持它的,于是进行尝试:安装 Pandoc、在 front matter 中写上 markup: pandocmarkup: pdc (YAML 格式)、把我已经写好的 LaTeX 代码拷贝进来、渲染网页,结果极其糟糕. LaTeX 代码前面的导言区完全被当成了注释不起任何作用,这就意味着整个 LaTeX 完全不能用. 另外,我行间公式的书写习惯为

1
2
3
\[
math display mode codes
\]

这也显而易见不能正确渲染——合着这一步操作唯一能做到的就是正确渲染 $math inline mode codes$ 而不需要加反引号 `$math code$`,而且还不能前一个美元符后面和后一个美元符前面有空格(当然大多数数学家写 LaTeX 就是这样做的),这还没包括一些在导言区引入的别的包里的数学符号(比如 extarrows 包里的 $\xlongequal$ 就无法被渲染). 而且那种比如我用 \include{other_section} 连接的多个 LaTeX 文件也不容易逐一复制进去. 所以这个方案对我来说聊胜于无. 尽管如此,已经配置好的部分我也没有删除,如果写一些只有寥寥几个简单数学公式的小文章兴许还能用得上.

另一个问题是在使用 Pandoc 渲染时,文章是没有目录的. 已有人在网络上对此问题进行了报告

对于 Pandoc,无法生成 TOC 是因为 hugo 在调用 Pandoc 将 Markdown 文件转换为 HTML 的时候,只使用了 --mathjax 选项,没有使用 --toc 选项…

我只想安安静静地跟着官方走,所以并不打算自己编译 Hugo 解决这个问题,而且 20 天前已经有相关的 pull request 了,虽然不知道会不会 merge,什么时候 merge,什么时候编译新版本. 无论如何我就先不管这个问题了.

为什么不试试原生的 Pandoc 呢?

直接用 Pandoc 转 LaTeX 呢?这样 \include{other_section} 连接的多个 LaTeX 文件的问题也不用操心了.

我先尝试了 LaTeX 转 HTML,由于这样一个单独的网页也没有配 CSS 不好放进这个博客已有的模板中,我又转而尝试了 LaTeX 转 Markdown,再加几行 front matter 放进 Hugo 的文件夹让 Hugo 的 blackfriday2 渲染. 这次的结果好看多了,但问题依然存在——

用 Pandoc 转换再加几行 front matter 不是什么大不了的事,写一个脚本就能分分钟搞定,关键的问题还在数学:很多导言区的包没有支持(比如画交换图的包 tikz-cd )、导言区定义的环境生成出来不好看、导言区自己定义的盒子显示不出来,很多数学符号如上一小节提到的 $\xlongequal$ 还是不支持等等.

到这里我便知道,世界上只有一个 LaTeX,让 Pandoc 完美支持 LaTeX 的所有功能简直是妄想,顶多是能顺利搞定人们最常用的那些标准的格式. 更何况本来 Markdown 和 HTML 就没有像 LaTeX 那样被设计,它们没有科学排版的初衷也不应该承担科学排版的责任. 这样一个“产品哲学”的问题想清楚以后,我便知道我该如何设计我的博客让它满足我心目中理想的样子.

硬上 PDF 呗

在网页中内嵌一个 PDF 文件对象成了我唯一的选择. 我的操作是在 /static/ 中建立一个到我原始文件的硬链接 $ ln /path/of/my/original/file.pdf /static/name.pdf,这样原始文件更新时这里可以同步更新(当然还需要用 Hugo 重新渲染再提交). 然后在相应的文章的 front matter 中加上一些额外的识别参数,在文章页 (single.html) 对其进行判断:

1
2
3
4
5
6
7
# /content/section1/section2/file_name.md
---
title: 文章题目
# ...省略其他参数
file_path: name.pdf
---
一些介绍性的文字(可选).
1
2
3
4
5
6
<!-- /themes/theme-name/layouts/_default/single.html -->
<div class="pdf-part">
    {{ with .Params.file_path }}
        <embed src="/{{ . }}" width="100%" height="700"/>
    {{ end }}
</div>

调整样式

按照心中预计的样子编写 CSS 文件,如果需要的话也写一些 JS. 就像 HTML 语言一样,关于它们的使用方法可以在任意相关的网站上找到,不是建立本站的难点,也不会成为这篇文章的重点.

关于目录的下拉样式主要参考了菜鸟教程, CSS 下拉菜单,目录序号的生成主要参考了Anastasia, Advanced Autonumbering Techniques with CSS.

然后就是我的文章排版,用了两端对齐,但是又有中文又有英文有时还有数学内容的文章就特别丑,中间的间距不均匀,我也不知道该怎么办.

注:2019 年 12 月 29 日我对所有的 CSS 进行了重写,使用了更为方便更为强大的 CSS3 技术,包括 flex 弹性盒子(主要参考的是阮一峰, Flex 布局教程:语法篇阮一峰, Flex 布局教程:实例篇)、transition 过渡动画以及 @media 多媒体查询,重新为小屏幕手机端写了新的 CSS,并做了响应式布局. 目前这个博客就更好看、更好用了——无论在电脑屏幕上还是手机屏幕上. 这里需要特别记录的一点是关于“视口”这个概念的理解,这篇知乎上的回答解答了我和提问者同样的困惑. 同时,之前的两端对齐的排版被我取消了,因为实在是太丑了,我宁愿让右侧不对齐.

再注:新的 CSS 在奇葩的内核下显示不正常,波及了 QQ 浏览器、UC 浏览器等. 试着解决了一次,但终究找不到问题的根源——明明我用这些浏览器跑CSS3 测试支持率很高,而且我用到的技术它都支持. 我懒得解决了,除非有人帮我定位一下问题,因为我 Linux 的生产环境没法装 QQ 浏览器,也不知道前端大佬们都是怎么测试的. 我的网站在 Firefox、Chrome 等浏览器上都显示得超级好,更何况这些问题就应该浏览器厂商解决,哪有留给前端解决的?留给我解决我就偏要留给用户解决. 要是所有前端都这么硬气,倒逼用户换成更现代的浏览器,我才不信浏览器厂商敢这么不作为. 反正我这个博客也没什么人看,就让用[显示不好的浏览器]的人不再来看我的博客好了. 补充于 2020 年 1 月 2 日.

再注:今天想起这件事,不死心又查了一下,怀疑是那些浏览器不支持 flex 的标准写法的缘故,于是找到这样一个解决此问题的工具:Autoprefixer,可以集成到 IDE 中使用,它有一个在线版本:Autoprefixer CSS online,能自动帮我们把那些非标准的写法补齐,而且还可以选择支持浏览器的覆盖范围,查询语法可以看这里. 这两个工具配合起来非常方便,我选择 last 4 version(可以支持 92.18% 的浏览器)试了一下,还是没有解决问题. 补充于 2020 年 4 月 10 日.

再注:还是不死心,到思否论坛上问了一下,然后在一个关键提示下,顺利找到问题并解决. 问题的根源在于 QQ 浏览器看到 flex 容器写着 height: 100vh 就想要帮我在一个屏幕高度内塞下所有 flex 项目,解决的办法是把 flex-shrink 调整到 0. 详见思否论坛的这个帖子. 补充于 2020 年 5 月 2 日.

注:今天对 CSS 的颜色变量进行了抽象提出,并在Michael “lampe” Lazarski, CSS Quickies: CSS Variables这篇教程的指导下完成了主题颜色的切换功能,这样一来本站便支持明暗两个主题了. 等过些日子好好配置出一套令人满意的明暗配色之后会在首页写个短消息公布一下. 补充于 2020 年 8 月 8 日.(啊刚刚皇马被曼城淘汰出局了,好伤心,瓦拉内两次失误啊.)

注:在 Color Scheme Designer 的帮助下,我重新设计了明亮模式的配色,只是稍微有一点点变动,基本感官没有变化. 接着我又一鼓作气,把黑暗模式的配色也完成了,主要参考了 Chethan KVS, The Ultimate Guide on Designing a Dark Theme for your Android appMaterial Design, Dark Theme 以及 Atharva Kulkarni, Dark Mode UI: the definitive guide,当然依然使用了 Color Scheme Designer. 黑暗模式的配色基本上是可以了,但我还是不是很满意. 一来觉得配色还是有不合理的地方,二是像一些图片在黑暗背景下显得很不协调——透明背景加黑色字体的图片几乎无法辨认、白色背景的又显得很突兀. 如果有人对配色比较在行,可以给我提供配色方案.

还应该完善的一个小功能是:应该用 prefers-color-scheme 技术判断一下用户的系统环境颜色,当然比较简单,在 main.js 里把初始化的那个 if 语句的最后一个 else 里写点东西判断一下就行,但我不是很想搞,还是觉得给所有人默认明亮模式比较好,或许等我的黑暗模式配色更好看一点再说吧. 补充于 2020 年 8 月 10 日.

prefers-color-scheme 已支持. 补充于 2021 年 8 月 27 日.

评论系统的支持

在这里我使用了以 GitHub issue 为工具的 utterances 来生成评论,原理非常简单,配置也非常容易,按照其官网的说明简单操作即可. 本站的所有评论存储在这里.

自定义的 404 页面

在这里遇到一个多语言页面的问题. GitHub Page 上说需要在根目录建立 404.html:

Create a HTML file named 404.html or create a new Markdown file named 404.md at the root level of your GitHub Pages repository.

Hugo 的官方也是这样支持的:

When using Hugo with GitHub Pages, you can provide your own template for a custom 404 error page by creating a 404.html template file in your /layouts folder. When Hugo generates your site, the 404.html file will be placed in the root.

然而真正操作的时候,Hugo 把 404.html 分别放到了不同的语言文件夹下. 这样一来我虽然可以定制不同语言版本的 404 页面,却没法在 GitHub Pages 上使用. 想了想也许可以手动建一个根目录下的 404.html 并用 JS 进行重定向,但觉得不爽所以我没有这样做,目前本站还是 GitHub Pages 默认的 404 页面. 网络上这个 issue 的解决可能是导致这个问题的根源,与此相关的还有这个讨论.

博客文章的加密与隐藏

2019 年 12 月有了给博客文章加密的需求,发现了 Hugo encryptor 这款工具,试了一下,效果良好,测试文章见这里.

这个工具的原理是在 layouts 目录中弄了一个 shortcodes 代码片段,里面包含了使用 crypto-js 这个工具的 javascript 片段用以判断密码是否输入正确并展示被加密文字. 之后 Hugo 生成静态网页时就会把 Markdown 中需要加密的部分的明文用那部分代码片段包裹,然后用 python 强行把 Hugo 生成的 public 目录下的 html 进行遍历,只要有被那个代码片段包裹的文字就直接用 python 加密替换进去,这样就完成了博客文章的加密. 上传后展示出来输入密码解密的过程就是纯 javascript 了.

而隐藏的需求就是让文章不在列表中显示出来,比如上面给出链接的那篇测试文章,它是“备忘”这个分区下的文章,却不能在分区列表页找到,也不会在首页的最近 5 篇中出现,用的是技术是在文章 Markdown 的 front matter 里引入新的参数,并在列表页用 where 函数判断.

这里需要额外注意的是 RSS 页也需要进行隐藏,因为 Hugo encryptor 这个工具只对 html 文件进行了加密,没有加密 xml 文件,如果不隐藏就会导致在 RSS 中可以看到密码和加密内容. 比如对上面给出链接的测试文章我故意没有在 RSS 列表页隐藏,这样在“备忘分区”的 RSS 中可以看到密码 <cipher-text data-password="PASS3word" style="display:none;"> 以及加密的内容.

总而言之,列表隐藏和文字加密可随意配合使用,列表隐藏但不文字加密、文字加密但不列表隐藏、文字加密并列表隐藏,这都是可以的;但只要文字加密就必须 RSS 隐藏.

这一小节补充于 2019 年 12 月 27 日.

今天把需要隐藏的文章也在 sitemap.xml 中的输出也隐藏掉了,这个修改是比较简单的. 在文档 Sitemap Templates 中有关于 sitemap 模板的详细介绍,尤其是多语言的网站,注意网站整体的 sitemap 是由 sitemapindex.xml 生成的,里面只包含所有不同语言分别的 sitemap;而每一种语言下的 sitemap 则是由 sitemap.xml 生成的.

加密和隐藏这件事事关信息安全,最最稳妥的做法肯定是永远不要在网上(个人博客里)写任何不想被人看到的东西,而不应该盲目信任任何技术手段或加密算法. 在这样的前提下,我还是摸索出了在 hugo 静态博客里写私密文章时需要注意的一些地方,这里简单罗列一下个人经验:

  1. 博客源码不能上网,即生成静态网页之前的包含 content 目录的那个原始目录;即便上网(比如在 GitHub 上做源码备份管理)也要注意选择 private 权限;
  2. 因此不能使用托管站点的 hugo 云上生成静态页面,必须在本地生成好,并且用本地的加密工具处理过一遍之后把处理好的 public 目录拿去直接托管;
  3. 注意加密工具的选择,考察加密工具所使用的加密算法以及加密流程,确保是成熟的、被广泛使用且经过密码学家验证的算法,不要自己发明加密算法或协议;
  4. 主题的设计上要配合加密工具的加密流程填补一些漏洞,比如加密工具只对 HTML 文件做加密处理,那么就要考虑到 RSS 生成的 XML 文件中是否包含加密内容,如果包含,则应在设计主题时考虑到这一点,要么直接不把含有加密内容的文章输出到 RSS 中,要么在输出时去掉加密内容的明文;
  5. 如果有隐藏文章的需求,需要排查:(主页、标签、归档等处的)文章列表、文章 url 是否有规律(如递增的数字序号)、RSS 中是否做了排除、是否禁止了搜索引擎收录、sitemap 中是否做了排除.

本段补充于 2022 年 11 月 6 日.

统计信息

最开始只用了 Google Analytics,没什么难的,本身 Hugo 就支持,只要在配置文件里把 Google Analytics 的代号写进去,有一个现成的语句片段可以用. 唯一要注意的是要用{{ if not .Site.IsServer }}判断一下别把本地测试的也统计进去就行了.

2020 年 3 月 6 日我听说了一个新的开源统计工具:Goat Counter,使用非常简单,我也马上就把它加进来了.

这一小节补充于 2020 年 3 月 6 日.

同栏目下的文章互链

我的博客站是有一些栏目的,比如目前就有“21 世纪的定理”、“消费指南”、“语言表达法”这三个栏目,栏目是一个希望自己持续更新的而且可以经常写的同类型题材的一类文章的集合,在这类文章的最后我想要放一个“友情链接”的部分,把同栏目下的其他文章进行汇总给出链接,方便读者在同一个栏目下进行跳转阅读. 之前文章比较少,就凑合着手动去添加,倒也费不了太多事,但最近“语言表达法”那个栏目的文章多了起来,就一直想写一个自动添加友情链接的功能.

最开始是想着用 Hugo 自带的分类功能,即那个 Taxonomy 的,我因为不需要 Tag、Category 等就把它禁掉了,见下面的第三个注释3,现在觉得可以自己定义一个简单的 column 项,参考这里试着写了一下,就是在配置文件 config.yaml 里写上一个新的 taxonomy,用冒号分隔单数与复数,然后在文章里的 front matter 部分使用,发现并没有出现我想象中的效果. 它那个 .golang 我是实在搞不清楚是怎么回事,而且还不能在 disableKind 里把这东西加上,因为一旦加上就完全不能用这个功能,而不加上又会生成单独的 taxonomy 页面. 于是就自己写,好在功能简单,很快就写出来了,前面计数的部分参考了这个讨论.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- themes/zero/layouts/_default/single.html -->
<!-- 下面是友情链接部分 -->
{{ if isset .Params "column" }}
    <h1>友情链接</h1>
    {{ $currentNode := . }}
    {{ $.Scratch.Set "counter" 1 }}
    {{ range .Site.Pages.ByDate }}
        {{ if eq .Params.column $currentNode.Params.column }}
            {{ if eq . $currentNode }}
                <li> 第 {{$.Scratch.Get "counter"}} 期:{{ .Title }}(就是这篇!)</li>
            {{ else }}
                <li> 第 {{$.Scratch.Get "counter"}} 期:<a href="{{ .Permalink }}">{{ .Title }}</a></li>
            {{ end }}
            {{ $.Scratch.Set "counter" (add ($.Scratch.Get "counter") 1) }}
        {{ end }}
    {{ end }}
{{ end }}

最后把中文的内容翻译翻译写成 {{ i18n "xxx" }} 的形式就可以了.

这一小节补充于 2020 年 7 月 29 日.

部署

GitHub Pages

按照官方文档进行部署,因为是个人博客,我用的是第一种 User/Organization Pages 类型. 操作也都极为简单,不再赘述. 如果遇到已经部署好了却打不开的情况可以尝试重新 push 一下.

Netlify

2019 年 12 月底,我发觉国内加载 Github Pages 的速度太慢了,于是有意识地寻找其他能部署的地方,发现 Netlify 也许还不错,就往这上面也部署了一份.

Netlify 支持 Github 登录,按照官方文档的说法可以把整个博客源码关联到 Netlify 网站项目上,Netlify 本身有一个环境可以运行命令生成静态文件,而且目前还可以选择环境,比如选择了 Ubuntu Xenial 16.04 image,它支持所有版本的 Hugo,那就相当于有一个 Ubuntu 系统在帮我跑 Hugo. 但有两个问题:

第一,这个方法和 Github Pages 不兼容,因为 Github Pages 用了 submodule,相当于仓库里面还有一个仓库,而 Netlify 管 Github 要授权的时候只授权了大仓库,这导致 Netlify 运行时会产生权限的错误. 这篇文章报告了这个问题并提供了一个看上去极其不自然的方法解决了这个问题. 我觉得那个方法实在是有点愚蠢,我没有更好的办法.

第二,如果让 Netlify 重新帮我用它那里的 Hugo 生成静态文件,那加密的部分怎么办?难道我要把加密的 python 文件上传到 Netlify 再设置一个钩子么?这听上去也很愚蠢,因为我已经有生成好的静态文件了,你只管帮我 host 就好了,搞这么麻烦干什么?

于是我就直接把静态文件的仓库 zerovip.github.io,也就是 Github Pages 用的那个 username.github.io 那个仓库关联到 Netlify 网站项目上了,也运行得非常好. 这时我才发现 Netlify 并没有想象中那样变得加载更快,我还是要寻找更好的解决方案.

这一小节补充于 2019 年 12 月 30 日.

其他

但我到最后也没找到令我满意的方案. 我理想的方案应该是,1.部署方便,直接把 Github Pages 用的仓库拿过来用就行;2.更新方便,Github 那边的仓库更新时这边也同步更新;3.自由,注册不需要手机号,不受中国大陆的监管;4.中国大陆的加载速度快. 事实证明,3 和 4 是矛盾的. 我了解过的方案有:

一,弄一个 CDN,但国外的 CDN 不满足 4,国内的 CDN 不满足 3.

二,反正是静态的博客,再找地方托管,考虑过:Firebase(不满足 1)、Gitee(不满足 3)、Coding(不满足 3)、Gitlab(不满足 4)等等.

于是干脆就不弄了.

这一小节补充于 2019 年 12 月 30 日.

Vercel

最近 Vercel 这个东西感觉有点流行起来了,中文世界里的一些平台上出现了越来越多关于它的很多讨论. 于是我又花了五分钟把我的博客多部署了一份,感觉它确实访问速度稍稍快一些. 它的部署也非常简单,用 GitHub 登录,从 GitHub 导入项目,在 GitHub 的相关 repo 中安装 Vercel app,然后就行了,后面有任何变化也会同步推送出来. 整体上感觉是 Netlify 的竞品,因为和 Netlify 一样它也支持各种框架——Hugo、Hexo、Jekyll、Next.js、Gatsby、Vue.js 等等等等等等,有一大堆都是我没听说过的东西. 原理应该和 Netlify 完全一样,可以设置部署的命令,环境变量等等,相当于是一台受到限制的 VPS.

我当然还是直接把静态仓库 zerovip.github.io 给部署上去了,因为涉及到用 python 脚本给渲染好的 html 加密. 但即便不涉及加密过程我也会这么部署的,因为 Hugo 本身就很强大,软件安装极其容易,生成静态文件速度极快,像我这样直接部署所有静态文件才不会因为版本号不同、环境不同等原因产生任何偏差,严格保证了所有内容完全一致. 像 Hexo 那种生成静态文件要好几秒且安装起来比较费事的框架可能这么搞稍微容易一点吧,这样搞的话本地不需要安装,只专注写 markdown 文档就好. 但缺点就是只能用别人的主题,不能像我这样自己写主题,对主题上的任何需求和不满意的地方也没法随时进行调整. 说起来,我对我自己的这个主题真的是相当满意,我对我的一切选择(选择了 Hugo、选择了自己写主题)都太满意了,前期辛苦一点点换来的是持久的幸福啊!

这一小节补充于 2020 年 8 月 21 日.

CloudFlare Pages

CloudFlare 也搞了一个 Pages 服务,不得不感慨,现在做托管的服务越来越多了. 于是又欢喜地把我的博客托管到了这上面一份.

这一小节补充于 2021 年 3 月 12 日.

是为记.

TODO 清单

  1. 评论系统(已完成)
  2. 适配手机的 CSS (重写 CSS 计划)(已完成)
  3. RSS 支持(已完成)
  4. 文章间的跳转(改为第 24 条,已完成)
  5. 文章的目录安置(右上角固定的可伸缩悬浮块)(已完成)
  6. 黑暗主题(更合理的配色,包括配一个黑暗模式出来,配色的时候注意一下文章中的黄色块还没对应黑暗模式)(已完成)
  7. Google analytics(已完成)
  8. tags, categories, series 等(不再考虑3. 在边栏用多级 section 分类已经足够,没必要增加更复杂的分类方式.)
  9. 本文中提到的中英文排版问题还没有解决(暂且先这样)
  10. 本文中提到的 404 页面问题还没有解决(无关紧要,见下面的第 25 条)
  11. 本文中提到的使用 pandoc 没有目录的问题还没有解决
  12. 数学类 overflow-x auto 后 overflow-y 自动出现的问题(已解决)
  13. 提交 sitemap 供搜索引擎收录(已完成)
  14. 把 head 中的标题改一下,在搜索引擎中显得好看一点(已完成)
  15. list 中写 head 标题的方法过于暴力,需要简化(不再考虑,不用太追求完美)
  16. 首页最近文章(已完成,显示 8 篇)
  17. QQ 浏览器、UC 浏览器等奇葩内核,<p> 完全消失的问题(已解决,问题的分析与解决办法见调整样式一节的最后一个注.)
  18. 国内加载速度太慢的问题(不再考虑)
  19. PWA,觉得很有潜力的技术,有时间的话会首先尝试它. 可以参考这篇文章.
  20. IPFS,不一定,有生之年试一下就好.
  21. Zero Net,Ditto.
  22. InstantClick,Ditto.
  23. CSS 颜色写成变量 (variables) 形式,重新配色,方便第 6 条黑暗模式重新加入考虑计划,见这个教程.(已完成)
  24. 用 hugo templates 把相同栏目的文章互链重新写一下.(已完成,但没有使用 templates. 见同栏目下的文章互链这一小节.)
  25. 对左边栏右上角的语言选项区域进行提前判断,没有翻译的文章不提供跳转链接,这样在本站自由点击跳转就不可能到达 404 页面了,上面第 10 条提到的问题就不是很需要解决了(已完成)
  26. 左边栏右上角不提供跳转的语言的颜色问题
  27. 语法高亮支持.(已完成)
  28. 站内搜索功能(非常不着急),可以参考这篇文章. 还有一个现成的模块可以参考一下:hugo-search-fuse-js.(见 65)
  29. 用一下 prefers-color-scheme. 可以参考这篇文章.(已完成,参考了Eureka 主题的写法.)
  30. 支持 webmention(还需要了解、学习并评估),可以参考这篇文章
  31. CSS 越写越多,修修补补,应该统一整理、化简一下(基本完成)
  32. 代码块是否需要折叠显示?参考:reuixiy’s meme
  33. 有什么办法可以不在文章中指定所属 section,根据目录结构在左边完成箭头的指示. 看下官方文档的 Breadcrumb Navigation 那里,毕竟官方文档那个网站就做到了. 可以阅读 #465.
  34. 换语言那里的 substr 之前应该数一下当前语言的字符串数量存进变量里,以能更好地应对普适的情况(使用 dict 更加优雅地完成了)
  35. 等所有 todo 都完成之后,这个主题才算是基本成型了,可以考虑一下要不要写个文章介绍一下自己的主题,以及要不要加进 hugo 的官方主题仓库,适当做个小推广呢?
  36. 一个超链接后面如果有标点符号,这里可能会形成断行,导致标点符号出现在下一行行首. 查到一个 js 的解决方法.
  37. 代码块的横向滚动,应该保持行号不动,只有代码块滚动. 是一个 CSS 的问题.
  38. 透明背景的图片在黑暗模式下应进行反色操作.(已完成. 需要在暗色主题下反色的图片只需在文件后缀名前加上 .i 即可,也就是说以 file_name.i.{png|jpg|jpeg|gif|webp|…} 的格式命名.)
  39. 考虑把豆瓣的书影音游的数据迁移到博客里,单独弄一个页面,使用Data Templates来做这件事.已完成
  40. 考虑列表页的分页问题?(如果文章太多了的话)参考:Pagination. 不再考虑,不用分页,一页就挺好.
  41. 暗色模式下加载新页面闪烁的问题.已完成
  42. 支持图片懒加载,使用 render hooks 技术.
  43. Open Graph 和 Twitter Cards 的支持,见Hugo Internal Templates.(暂不考虑,觉得不是很有必要. 不过如果打算宣传自己的主题的话 (35) 还是要加上.)
  44. 使用 Page Bundles 重新整理整个博客的目录结构.(已完成,写了一个脚本来做这件事.)
  45. 用 pandoc 渲染的文章也没法用 render hooks 的问题.(11)
  46. utterence 评论插件的主题更换脚本应在加载完成后运行. 或者干脆用 invert 的办法暴力转换算了……
  47. 无障碍友好问题需要关注! 重新配色以方便色盲和色弱,以及多多使用 HTML 5 无障碍标签,让使用 TalkBack 的人可以方便使用.
  48. 行内代码样式难看,尤其是亮色模式下,配色需要重新调整,亮色、暗色的代码应该使用不同的配色方案.
  49. 列表页不好看,每一个条目的渲染还需要再重新设计一下,用上点漂亮的图标如 OpenMoji,主页的列表需要标注来自哪个 section,摘要的长度也需要好好考虑. 可以考虑设计成卡片的样子.
  50. 文章页也应该渲染摘要,可以简简单单地放在发表时间、字数那一行的下面.
  51. 关于加密与执行外部命令,阅读 #7547,AES 的不安全性,secretbox 的问题.
  52. 阅读 #847 关于改 hugo 代码让它执行外部命令的 pr,试着学习 hugo 源码.
  53. 生成的 html 里很多空格、换行,要不要全用上 {{- xxx -}} 呢?
  54. 在 sitemap 里去掉隐藏的文章. 加一个新的变量?(IMPORTANT)(已完成,没有加新的变量,就使用了 list_hide
  55. 关于和记录这两个一级列表板块,要不要去掉这两个列表页?
  56. 关于和记录这两个一级列表板块,要不要去掉它们的 rss?
  57. Mathjax 换 Katex.
  58. 学习 commonmark 规范,写一篇示例文章,包含所有可能的格式及其嵌套,在此基础上重新把样式表设计一下.
  59. RSS 里的 url 都应该用绝对路径,但是有些脚注什么的是 #fn:1 这种. workaround:在 baseof.xml 里对 {{ .Content }} 做修正?
  60. RSS 里不应使用 role attribute,但是脚标的生成就含有 endnotes, backlink, noteref 等 role. workaround:在 baseof.xml 里对 {{ .Content }} 做修正?
  61. RSS 里不应使用 aria-hidden attribute,但 pandoc 渲染的不走 render hooks,图片的 figcaption 都加了这个字段. workaround:在 baseof.xml 里对 {{ .Content }} 做修正?
  62. 把 validator.w3.org 上建议的对 RSS 的改进都给修了……Invalid HTML: Unexpected end tag (p). 大部分都修了,因为 shortcode 那个段落的 </div> 没有另起一行导致的. 但还剩 4 个……咋回事呢?
  63. 渲染图片的 render-hooks,会被用 <p> 标签给包裹起来,如果 render-hooks 里面用的是 <figure> 标签,这就这不太对. 感觉 Hugo 很多细节上的处理都不是很讲究,当然 GoldenMark 的部分我也不懂. 需要对整个 markdown 转 html 的规范与流程做一个深入的调查,尤其是 render-hooks 相关以及 shortcodes 相关.
  64. 关于 pjax 的部分,一直都想做,现在知道了这个关键词,可以多查一查,看看怎么实现. 已经有的 js 库比如 swup、barba 等,应该不太难.
  65. 关于站内搜索,又发现了一个非常好的工具:pagefind. 读了大致的介绍与文档,就直接确定了,这就是我想要的东西,就决定用它了!有时间的时候搞一搞,同时第 28 条 todo 也就可以废弃掉了.

  1. 不同之处在于,由于 Python 是解释型语言,Django 会在客户端访问时对客户端访问的一个网页进行渲染并发回客户端;而 GoLang 是编译型的,只好编译成 Hugo 供人们在每写完一篇文章后对整个网站的所有网页重新渲染好,成为静态网页后进行发布). ↩︎

  2. 自 2019 年 11 月 26 日 Hugo 升级到 0.60.0 版本后,默认的渲染引擎已经从 blackfriday 换成了 goldmark. ↩︎

  3. 2020 年 1 月 1 日尝试着做了一下,主要是要看官方文档模板那一章,最重要的是分类模板那一节. Taxonomy 其实非常灵活,把它搞定只需要解决 3 个方面的问题:Taxonomy Term、Taxonomy List 和文章中关于 Taxonomy 的显示. Taxonomy Term 是某个分类页,比如【导演】页列出库布里克、希区柯克和黑泽明,比如【世纪】页列出 20 世纪、21 世纪,比如【标签】页列出青春、访谈、计算机;而 Taxonomy List 是具体分类项页,这是名副其实的列表页,比如【希区柯克】页列出所有希区柯克的电影,比如【21 世纪】页列出所有 21 世纪的电影,比如【青春】页列出所有含“青春”这个标签的文章. 这不难理解,但问题在于多语言. 按我的设想应该是同一个网站同样的内容被翻译成不同语言,所以英文的【导演】页应该是【directors】页,这在地址栏上不像想象中运作,在 “…/zh/导演/” 页点击英语按钮会转到 “…/en/导演/”,这个问题没想到很简单的解决办法,目前想到的解决办法是每个分类页、分类项页都用 _index.md 写出多语言的形式,地址栏永恒保持英文,不过这一想就觉得困难. 这也是我决定放弃这个功能的一个原因. ↩︎ ↩︎

友情链接(该栏目下的其他文章)

  1. 第 1 期:Hugo 静态博客建站记 - 自写主题(就是这篇!)
  2. 第 2 期:博客记录书影音——Hugo 的 Data Templates - 自写主题
  3. 第 3 期:如何让静态网站的暗色主题换页不闪烁 - 自写主题
  4. 第 4 期:Hugo 如何让图片在暗色主题下反色 - 自写主题