使用 Hugo 搭建静态站点#

在本地知识库软件编写文章在很多时候已经足够满足我参考的需要, 但若遇上呈给别人台鉴的情况, 终归不够方便. 另外文章虽在 GitHub 保存, 但其缺失对元数据的整理功能, 对于必要的 Markdown 拓展语法支持也不佳, 更关键的是, 其使用的 KaTeX 数学引擎较为简陋, 难以招架文章中出现的连篇累牍的公式. 无奈之下, 恐怕建立一个阅览站点成了唯一可行的选择.

本篇文章着重介绍站点本身的配置与搭建, 关于内容的编写与发布请看 这篇文章.

主题选择#

我希望站点整体上是静态的, 没有过分到眼花缭乱的小动画与动态效果, 主题风格简洁严肃, 就像一本书或一页纸一样, 仅用来供人阅览而不喧宾夺主. 最后我选择的主题是 book, 效果和它的名字一样沉静. 然而不得不说的是, 或许我下面的要求过分了点, 没有让我完全满意的主题, book 也不例外. 于是只好对它进行一番改造了.

特殊语法支持#

一些 Markdown 方言提供了特别有益的拓展语法, 例如 admonitions, 我们希望尽可能地使其可用. 幸运地是 book 主题本身就支持这一语法, 因此我们并不需要大费周章配置. 但如果是不支持此语法的主题, 可以额外安装一个 hugo-admonitions 主题, 并将其置于主要主题之前, 这样便可覆盖原主题来提供 admonitions 支持, 如下

theme = ["hugo-admonitions", "book"]

一定注意 hugo-admonitions 应该放置于主要主题 – 这里是 book– 前面, 因为主题的使用是从前到后的.

另外, 这就是一个 admonition.

定制字体#

hugo 管理站点的方式是 联合文件系统. 简单地说, 站点的各项文件配置都是分层的, 放置在上层的文件将会覆盖下层. 目录层级如下所示.

<root>
<root>/themes/<theme-a>
...

因此, layouts/baseof.html 将会覆盖 themes/book/layouts/baseof.html, 其余目录和文件皆是同理.

我希望站点正文字体使用衬线体, 以此增强内容的出版物质感, 更有 “文章感”. book 主题提供了设置字体的 定制文件 assets/_fonts.scss, 我们修改它即可更换字体.

将下面的内容填入此文件中

/* assets/_fonts.scss */

@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;600;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://cdn.jsdelivr.net/npm/cn-fontsource-source-han-serif-sc-vf@1.0.9/font.css');
@import url('https://cdnjs.cloudflare.com/ajax/libs/Iosevka/6.0.0/iosevka/iosevka.min.css');


:root {
  --paper-font-body: "Source Serif 4", "Source Han Serif SC", "Noto Serif SC", serif;
  --paper-font-ui: "Inter", system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial;
  --paper-font-mono: "Iosevka Web", "Iosevka", "JetBrains Mono", ui-monospace, "SFMono-Regular", "Menlo", monospace;

  --normal-wght: 300;
  --mono-wght: 400;
  --strong-wght: 600;
}


html, body {
  font-family: var(--paper-font-body);
  font-weight: var(--normal-wght);
}

strong {
  font-weight: var(--strong-wght) !important;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--paper-font-ui);
}

code, pre {
  font-family: var(--paper-font-mono);
  font-weight: var(--mono-wght);
}

这将设置正文字体为衬线体 (中文 Source Han Serif SC, 西文 Source Serif), 标题为无衬线体 (Inter), 代码块为等宽无衬线体 (Iosevka); 并将加粗 strong 字重改大来提高可见性. 以上 4 个字体都配置了 CDN.

添加文章修改时间#

book 中同样也有修改时间的原生支持, 但是需要与 GitHub 仓库进行集成, 这是一个比较苛刻的条件, 我无法做到, 因此我将手动实现一个放置在左下角的文章修改时间. 如果你对 book 中提供的修改时间显示有兴趣, 可以去看 站点配置enableGitInfoBookLastChangeLink 两个字段. 只有这两者都被正确配置, 时间显示才会开启.

一般来说, 几乎每个主题都会预留 注入点 (inject). 它们是主题中空的占位文件, 等待着被来自上层的文件覆盖. 它们已经被主题中的各项文件引用, 但因为是空的, 不会有任何效果, 当它们被覆盖后, 新文件的内容就会出现在站点中. 这是一种非侵入式的个性化手段. book 主题中提供的注入点可以在 GitHub 自述文件 中找到.

我将利用 book 在页面内容之后的注入点 layouts/partials/docs/inject/content-after.html 来手动实现一个修改时间显示, 它会读取文档元数据 date 字段来格式化为相应的日期显示.

<!-- layouts/partials/docs/inject/content-after.html -->
{{ $dateFormat := default "January 2, 2006" $.Site.Params.BookDateFormat }}

{{ if and .IsPage .Date }}
  <div class="flex">
    <div  style="font-style:italic;">
      <span class="overline"></span>
      <span style="display:block;">Created in {{ .Date.Format $dateFormat }}</span>
      <span style="display:block;">Last modified in {{ .Lastmod.Format $dateFormat }}</span>
    </div>
  </div>
{{ end }}

这段代码将从文档元数据字段 date 中读取日期, 并按站点设置 BookDateFormat 中规定的样式格式化. 上次修改日期 lastmod 由 hugo 提供, 它总是存在, 当 markdown 的 front matter 中不存在此字段时, 它的值将回退到 date 的值1. 最后的显示效果为左下角的 Created in January 2, 2006Last modified in January 2, 2006, 分两行显示. 斜体, 上方覆盖一条与文字等长的灰色分割线.

其配套的样式 overline 定义在 assets/_custom.scss

/* assets/_custom.scss */

.overline {
  display: inline-block;
  position: relative;
  display: block;
  margin-top: 16px;
}

/* 伪元素作为横线 */
.overline::before {
  /* background-color: currentColor; */
  background: var(--gray-200);
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width:100%;
  height: 2px;
  z-index: 2;
  pointer-events: none;
  transform: translateY(-200%);
}

/* skipping... */

未完成标记#

如果文章未完成, 我会在文章的 tag 中添加一个 undone. 我们将要给所有未完成的文章添加一个标记, 使读者能够意识到这一点, 否则可能会对只有半拉的内容产生疑问.

我们使用 book 在页面内容之前的注入点 layouts/partials/docs/inject/content-before.html 来实现.

<!-- layouts/partials/docs/inject/content-before.html -->
{{ $tags := .Params.tags }}
{{ $hasUndone := false }}

{{ range $t := $tags }}
  {{ if eq (lower (print $t)) "undone" }}
    {{ $hasUndone = true }}
  {{ end }}
{{ end }}

{{ if $hasUndone }}
  <div style="display:flex; margin-bottom:5px;">
    <span>[</span>
    <span class="status-undone">未完成</span>
    <span>]</span>
  </div>
{{ end }}

这段代码会尝试在 tag 列表中查找 undone, 如果找到了, 就会在标题之上显示一个 [未完成], 字体颜色为红色, 醒目地警告读者.

其配套的样式 status-undone 同样定义在 assets/_custom.scss

/* assets/_custom.scss */

/* skipping... */

.status-undone {
  color: var(--color-accent-caution);
  font-family: var(--paper-font-ui);
}

数学支持#

关于为 hugo 添加 MathJax 支持, 请看 这篇文章.

主页面#

访问网址时进入的页面为主页面, 默认情况下主页面是空的, 你可以在 content 目录中新建一个 content/_index.md 来定制你主页面的内容. 我希望在主页中呈现一个导航菜单, 按文章 tag 分类. 要做到这一点, 让 hugo 自行渲染是最方便的, 免于我们手动编写代码.

book 主题中自带了一个导航页, 但是隐藏得比较深. 这一部分页面编写在 book 仓库的 layouts/_partials/docs/taxonomy.html 中, 我们直接调用这一部分代码即可. 我将使用 hugo 提供的 Shortcode 机制, 它能让我们在 Markdown 中引入 HTML 或其他逻辑.

layouts/shortcodes 中新建 book-taxonomy.html, 如下

<!-- layouts/shortcodes/book-taxonomy.html -->
{{ partial "docs/taxonomy" .Page }}

这样你就可以在 Markdown 中使用 {{< book-taxonomy >}} 来渲染一个按 tag 分类的导航页.

最终配置文件#

最终使用的 hugo.toml 配置文件如下所示

baseURL = 'https://example.org/'
languageCode = 'zh-cn'
title = "EiEddie's Mind"
# theme = ["hugo-admonitions", "book"]
theme = ["book"]


[params]
math = true
BookTheme = 'auto'
BookDateFormat = 'January 2, 2006'


[markup.tableOfContents]
startLevel = 2
endLevel = 4
ordered = false

[markup.goldmark.renderer]
unsafe = true

[markup.goldmark.extensions.passthrough]
enable = true
delimiters = { block = [['$$', '$$']], inline = [['\(', '\)']] }
Created in January 4, 2026 Last modified in February 3, 2026