本站主题使用与配置说明

适用版本:Hugo 0.104.3(extended)+ 自定义主题 writing
最后更新:2026-09-10


目录

  1. 项目概览
  2. 本地运行
  3. 主题设置
  4. 文章加密机制
  5. 相册 · 飞书配置
  6. Vercel 部署
  7. 常用命令速查

一、项目概览

技术栈

静态生成器 Hugo 0.104.3 extended
主题 themes/writing(本站专用自定义主题,非公开主题)
加密工具 hugoArticleEncryptor(AES-GCM)
相册数据源 飞书多维表格 → GitHub Actions → data/album.json
部署 Vercel(GitHub 推送自动触发)

目录结构

Blog/
├── config.toml              # 站点主配置(改这里)
├── build.sh                 # Vercel 构建脚本(下载加密工具并执行加密)
├── vercel.json              # Vercel 配置:指定 HUGO_VERSION
├── requirements.txt         # 相册脚本依赖(requests / Pillow)
│
├── content/                 # 文章内容
│   ├── posts/               # 文章(加密工具只处理这个目录)
│   └── donate/              # 打赏页(自带 assets)
│
├── data/
│   └── album.json           # 相册数据(由 Actions 生成,入库)
│
├── static/
│   └── album/               # 相册图片(full / thumb / orig 三档)
│
├── scripts/
│   └── sync_album.py        # 相册同步脚本
│
├── themes/writing/
│   ├── config.toml          # 主题默认参数(⚠️ 只有 [params] 会生效)
│   ├── layouts/             # 模板
│   ├── static/dist/         # 主题 CSS / JS
│   └── theme.toml           # 主题元数据(不参与配置合并)
│
└── .github/workflows/
    └── album-sync.yml       # 相册自动同步

页面路由

路径 模板 说明
/ layouts/index.html 首页,带头图 + 文章列表
/posts _default/list.html 归档
/posts/xxx.html _default/single.html 文章页(permalink 见 config.toml)
/album _default/album.html 相册网格 + 画廊灯箱
/navigation _default/navigation.html 导航页
/about _default/about.html 关于页
/404 404.html 404 页

二、本地运行

前置要求

  • Hugo 0.104.3 extended(普通版缺 WebP 处理能力,相册图片会出问题)
  • Python 3.12 + requests / Pillow(仅相册同步脚本需要)

常用命令

# 本地预览(默认 http://localhost:1313)
hugo server --themesDir themes

# 构建到 public/
hugo --themesDir themes --cleanDestinationDir

# 查看最终合并后的配置(排查配置不生效时很有用)
hugo config

⚠️ Windows 下 Hugo 的 -d 参数用相对路径(如 -d ../out)会落在意料之外的位置,
建议用绝对路径或直接在站点根目录构建。


三、主题设置

3.1 站点主配置 config.toml

baseURL = "https://www.joker.cc/"
theme   = "writing"
title   = "Joker"
languageCode = "zh-CN"

[params]
  description = "每一个平凡的日落日升..."
  keywords    = ["Joker","Joker主页","Joker博客"]
  Name        = "Joker"
  mail        = "www@joker.cc"
  feed        = "/index.xml"
  page_view_conter = true        # 文章字数统计

[permalinks]
  posts = "/posts/:slug.html"    # 文章 URL 格式

[markup.goldmark]
  [markup.goldmark.renderer]
      unsafe = true              # 允许 Markdown 里写原始 HTML

[author]
  name  = "Joker"
  email = "www@joker.cc"

3.2 主题参数 themes/writing/config.toml

⚠️ Hugo 0.104.3 实测:主题目录下的 config.toml 只有 [params] 段会被合并进最终配置。
其他顶层段(baseURL[markup]、自定义段)一律不合并,写了也不生效。
所以主题配置里只放 [params],站点根目录的 config.toml 才是配置主战场。

合并规则:

  • 站点 config.toml 优先级高于主题配置,同名键以站点为准
  • 数组类(如 [[params.menu]]整体替换,不做逐项合并
  • Hugo 会把 params 键名统一转成小写——配置里写 avatarAlt,模板里只能读到 .avataralt
    所以这里一律用全小写命名

导航菜单

[[params.menu]]
  name = "归档"
  url  = "/posts"
[[params.menu]]
  name = "导航"
  url  = "/navigation"
[[params.menu]]
  name = "相册"
  url  = "/album"
[[params.menu]]
  name = "关于"
  url  = "/about"

桌面导航条与移动端下拉菜单共用这一份,模板自动插入 | 分隔符。增删条目直接改这里,不用动模板。

关于卡片(profile)

[params.profile]
  hitokoto = true                 # 是否拉取「一言」填充头像右侧签名位

  [params.profile.avatar]
    src  = "/images/i.webp"
    alt  = "Joker"
    link = "/about"

[[params.profile.links]]
  name = "联系"
  icon = "icon-mail"
  url  = "mailto:www@joker.cc"
[[params.profile.links]]
  name = "订阅"
  icon = "icon-rss1"
  url  = "/index.xml"

icon 取值见 themes/writing/static/dist/css/style.min.css 内置的 icomoon 图标类名。

3.3 主题外观模式

主题支持三种模式,通过 cookie 记忆,也可用 URL 参数强制切换:

模式 body class 切换方式
亮色 (默认) ?theme=light
暖色(护眼) eye-protection-mode ?theme=warm
暗色 theme-dark ?theme=dark

首屏有一小段内联脚本在 body 首帧绘制前同步应用主题,避免刷新时闪一下亮色(FOUC)。
这段脚本在 layouts/_default/album.html 等页面模板顶部,不要删除

暗色模式下的配色覆盖集中在 static/dist/css/theme-components.css
body.theme-dark 段,改配色去那里改。

3.4 写文章

hugo new posts/我的文章.md

archetypes 默认模板:

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true          # 记得改成 false 才会发布
---

文章里图片可直接用 Markdown 语法,主题有 _markup/render-image.html 接管渲染。


四、文章加密机制

4.1 原理

使用 hugoArticleEncryptor(Hugo Encryptor 的 Go 实现):

  • AES-GCM 加密整篇文章内容
  • 只处理 content/posts/(或 content/post/)目录下的文章
  • 读者输入正确密码后前端解密显示;密码输入一次后,访问其他加密页无需再次输入
  • 工具会自动把解密所需的 secret.html shortcode 与 AESDecrypt.js 放进主题对应目录

4.2 加密步骤

第一步:在文章里用 secret 标签包裹要加密的内容

---
title: "这是加密文章"
date: 2023-07-11T01:53:48+08:00
---

              ← 必须放在 secret 标签之前

抱歉,内容受密码保护!

⚠️ `` 不能省。它保证列表页(首页 / 归档)只显示摘要,
不会把加密区块泄露到列表页摘要里。

第二步:本地加密(可选,Vercel 会自动做)

releases 下载对应平台的二进制,
放到项目根目录运行:

# Windows
.\hugoArticleEncryptor-windows-amd64.exe
# Linux / macOS
chmod +x ./hugoArticleEncryptor-linux-amd64
./hugoArticleEncryptor

运行后文章正文会被替换成密文,然后正常 hugo 构建即可。

第三步:本地预览效果

python3 -m http.server -b 0.0.0.0 -d public 1313

打开 http://localhost:1313/ 查看。

4.3 注意事项

说明
仓库必须私有 官方明确建议。加密保护的是页面访客,源码仓库若公开,密文与解密逻辑都可见
不要手改 secret.html 该文件由加密工具生成并覆盖,手改会在下次构建时丢失
本地仓库没有 secret.html 是正常的 构建时由工具动态生成,无需提交到仓库
当前 content 里没有加密文章 机制已就绪,需要时按上述格式写即可
密码不要写进 config.toml 密码写在文章 front matter 下方的 secret 标签里,别集中配置以免泄露

五、相册 · 飞书配置

相册数据流:

手机飞书 App 加/改记录
    ↓ 飞书自动化(webhook)
GitHub Actions(repository_dispatch)
    ↓ 拉飞书 → 下载图片 → 三档压缩 → 提 EXIF
data/album.json + static/album/
    ↓ Actions 自动 commit & push
Vercel 检测推送 → 重建 → 相册页更新

5.1 创建飞书自建应用

  1. 打开 https://open.feishu.cn/app → 「创建企业自建应用」
  2. 「凭证与基础信息」里复制 App IDApp Secret
  3. 「权限管理」里开通:
    • 多维表格类:bitable:app:readonly(脚本只读,够用)或 bitable:app
    • 云文档 / drive 类:带「下载」字样的必须勾(否则拉附件 401)
  4. 「版本管理与发布」→ 创建版本 → 申请发布(个人应用一般秒过)

    权限点完开通后,不发版本不生效

5.2 创建多维表格

新建多维表格(不是普通电子表格),字段如下(字段名必须完全一致,代码靠名字取值):

字段名 类型 必填 说明
照片 附件 一条记录可挂多张图
说明 文本 照片下方第一行,可留空
公开 复选框 勾上才进博客;不勾就是私密存档

「地点」字段已废弃。相册只展示「拍摄时间 + 拍摄参数 + 说明」,
GPS 与经纬度一律不读取、不展示。

把应用加为协作者(最容易漏,漏了报 1254043):
表格右上角 ... → 「更多」→ 「添加文档应用」→ 搜应用名 → 权限给「可编辑」。

5.3 获取表格 ID

地址栏形如:

https://xxx.feishu.cn/base/BasCnxxxxxxxxxxxxxxxx?table=tblxxxxxxxxxxxx&view=vewxxxxxxxx
                            └──── app_token ────┘        └── table_id ──┘

5.4 配置 GitHub Secrets

仓库 → SettingsSecrets and variablesActions → New repository secret:

Name Value
FEISHU_APP_ID App ID
FEISHU_APP_SECRET App Secret
FEISHU_APP_TOKEN 上一步的 app_token
FEISHU_TABLE_ID 上一步的 table_id

⚠️ 这四个值只存在 GitHub Secrets,不要写进任何文件、不要提交
本地调试可放项目根目录 .env(已在 .gitignore 中)。

5.5 飞书自动化(实时触发)

飞书表格 → 「自动化」→ 新建:

  • 触发器:记录新增或修改时
  • 动作:发送 HTTP 请求
  • MethodPOST
  • URLhttps://api.github.com/repos/zhufacai/Blog/dispatches
  • Headers:逐个添加,Key 与 Value 分开填
Key Value(只填这一列的内容,别带 Key 名)
Authorization Bearer 你的PATBearer一个空格,再贴 PAT)
Content-Type application/json
Accept application/vnd.github+json(可省略,见下方说明)
  • Body{"event_type": "album-updated"}

🔴 最容易踩的坑:不要把 Key: Value 整行复制进「值」框。
飞书的 header 是 Key 和 Value 两个分开的输入框,
若把 Accept: application/vnd.github+json 整串粘进 Value 框,
值里就混进了 Key 名、冒号甚至换行,Go 的 http 库会直接拒绝并报:

net/http: invalid header field value for "Accept"

这是请求还没发出去就失败了,Actions 自然不会触发。
修法:把 Value 清空,手打一遍;或者直接删掉 Accept 这一行
(GitHub 该接口不强制要求它,只留 Authorization + Content-Type 也能正常触发)。

⚠️ 出参解析设为「无」。GitHub 该接口成功返回 204 No Content
飞书若按 JSON 解析会报「出参返回格式不正确」——这是假报错,实际已经触发成功了

⚠️ 触发 repository_dispatch 必须用 Personal Access Token
secrets.GITHUB_TOKEN 无效。建议用 fine-grained PAT,只授权这一个仓库的
Contents: Read and write + Metadata: Read

5.5.1 当前自动化清单

飞书里「工作流」和「自动化」是同一个东西的两种叫法,底层实体都是 workflow,
ID 一律以 wkf 开头。两个入口看到的是同一份数据,不存在两套系统。

相册表现有 2 条自动化,都在启用状态:

# 标题 触发器 监听字段 workflow_id 管什么
1 相册同步:飞书新增记录 → GitHub Actions 新增记录时(AddRecordTrigger 照片 wkfvQEQWk6bitYhi 加照片
2 相册同步:飞书修改记录 → GitHub Actions 修改记录时(SetRecordTrigger 照片 wkfMbV3uHf1Zi8mE 换照片 / 删照片

不存在重复触发AddRecordTrigger 只管新增、SetRecordTrigger 只管修改,
两者事件互斥,同一次操作只会命中其中一条。
(只有 ChangeRecordTrigger 会同时监听新增 + 修改,本仓库没用它。)

即便两条都命中也无害——Actions 侧配了 concurrency: group: album-sync
后到的 run 会取消前一个,最终只跑最后一次,而相册同步是幂等的。

配置要点

  • 监听字段「照片」,附件增删改都会命中 ✅
  • 请求头只有 Authorization + Content-Type,没有 Accept,绕开了 5.5 那个坑 ✅
  • 出参解析设为「无」,不会把 GitHub 的 204 当错误 ✅
  • Body {"event_type":"album-updated"} 与 yml 里 types: [album-updated] 一字不差 ✅
  • PAT 与第 1 条一致 ✅
  • 状态 enabled
  • trigger_control_listpasteUpdate / automationBatchUpdate / openAPIBatchUpdate
    这是「批量更新场景是否纳入触发」的控制项,保持默认即可

命令行查看:

# 列出某个多维表格的全部自动化
lark-cli base +workflow-list --base-token <app_token>

# 看某一条的完整定义(重点核对 headers / raw_body / response_type)
lark-cli base +workflow-get --base-token <app_token> --workflow-id wkf...

# 改完要单独启用(update 不会自动启用)
lark-cli base +workflow-enable --base-token <app_token> --workflow-id wkf...

5.5.2 删除照片:先删照片,再删记录

🔴 飞书多维表格自动化没有「删除记录」触发器。
全部触发器只有 7 种:AddRecordTrigger(新增)、SetRecordTrigger(修改)、
ChangeRecordTrigger(新增或修改且满足条件)、TimerTrigger(定时)、
ReminderTrigger(日期提醒)、ButtonTrigger(点按钮)、LarkMessageTrigger(收消息)。
删掉整行 → 飞书不发任何通知 → Actions 不跑 → 博客上的照片会一直留着。

脚本本身是支持删除的:sync_album.py 每次都拿飞书全量记录跟 data/album.json 对账,
飞书里已经没有的 key 会被剔除,同时删掉 thumb / full / orig 三档文件,
末尾还有一轮孤儿文件兜底清理。所以只要能跑一次同步,删除就会生效——
问题只在「没人通知它跑」。

推荐做法(零改动):先删照片,后删记录

  1. 打开那一行,把「照片」字段里的附件删掉(或整列清空)
    → 「照片」字段发生变化 → 命中第 2 条「修改记录」自动化 → 立即同步,博客上的照片消失
  2. 确认博客更新后,再把这一整行删掉
    → 删行本身不触发同步,但这行已经没有照片了,删不删对博客没有影响

顺序不能反:先删行的话,那一行连同能触发同步的字段一起没了,
博客上的照片要等到下一次定时兜底(北京时间凌晨 4 点)才会消失。

5.5.3 排查:Actions 没被触发

按顺序验证,每一步都能独立定位问题

① 先用 curl 绕过飞书,直接验证 PAT 和接口

curl -i -X POST \
  -H "Authorization: Bearer 你的PAT" \
  -H "Accept: application/vnd.github+json" \
  -H "Content-Type: application/json" \
  -d '{"event_type":"album-updated"}' \
  https://api.github.com/repos/zhufacai/Blog/dispatches

看返回的状态码:

状态码 含义 处理
204 ✅ 成功 去 Actions 页面看有没有新 run;没有则查第 ③ 步
401 Bad credentials PAT 无效或已过期 重新生成 PAT
403 权限不足 PAT 需 Contents: Read and write
404 仓库不存在 / PAT 无权访问此仓库 确认 PAT 授权范围包含该仓库

curl 成功但 Actions 没动静 → 问题在 workflow 配置,不在飞书和 PAT。

② 确认 workflow 文件在默认分支上

repository_dispatch 只在仓库默认分支(通常是 main)的 workflow 文件上触发。
文件在别的分支、或改动还没合并到默认分支,都不会触发。

③ 确认 event_type 完全匹配

Body 里的 album-updated 必须与 album-sync.yml 里的
types: [album-updated] 一字不差

④ 确认 Actions 已启用

仓库 → Settings → Actions → General,确认允许运行 workflow。

⑤ 手动触发一次做对照

仓库 → Actions → Sync Album → Run workflow

  • 手动能跑 → 说明 workflow 本身没问题,问题在触发链路(PAT 或飞书配置)
  • 手动也跑不了 → 问题在 workflow 文件或 Actions 权限

5.6 GitHub Actions

.github/workflows/album-sync.yml 触发方式:

触发 说明
repository_dispatch (type=album-updated) 主路径,飞书实时触发
schedule: "0 20 * * *" 兜底,每天 UTC 20:00(北京时间凌晨 4 点)
workflow_dispatch 手动,可勾 force 全量重跑

GitHub 的 schedule 在高负载时会被延迟甚至丢弃,不能作为同步保障,只能兜底。

workflow 里已写死 Vercel 部署保护所需的 commit 身份:

env:
  GIT_USER_NAME: "Joker"
  GIT_USER_EMAIL: "qop@live.com"     # 必须是 GitHub 账号关联的邮箱

以及推送权限:

permissions:
  contents: write      # actions/checkout 默认只有 read,不加这个 push 必报 403

并发控制:

concurrency:
  group: album-sync
  cancel-in-progress: true    # 连续触发时取消上一 run,避免打架

⚠️ 改了 sync_album.py 的提取逻辑后,下一次运行会全量重建: 已有的 key 全部匹配不上,每张图重新下载、重新提取 EXIF。 72 张约 3~4 分钟,日志里会刷一大片「新增」——属正常现象,不是重复

5.7 数据格式

data/album.json(Hugo 通过 .Site.Data.album 读取):

{
  "photos": [
    {
      "id":     "recvuCBfT4uPdC-4",
      "thumb":  "/album/thumb/xxx.webp",
      "full":   "/album/full/xxx.webp",
      "orig":   "/album/orig/xxx.jpg",
      "width":  873,
      "height": 1280,
      "date":   "2023-10-01 19:40",
      "model":  "Redmi K60",
      "exif":   ["f/1.8", "1/605s", "ISO 49", "5mm"],
      "params": "Redmi K60 · f/1.8 1/605s ISO 49 5mm",
      "caption": ""
    }
  ]
}
字段 说明
full 页面展示用,长边 1280 的 WebP(q82)
thumb 缩略图,长边 400
orig 原图存档,页面不加载
date EXIF 时间;无 EXIF 时从文件名推断
model / exif 结构化机型与参数,页面优先用这两个
params 旧格式整串,仅作老数据兜底
caption 飞书「说明」字段

5.8 常见问题

现象 原因 处理
1254043 应用没加为表格协作者 回到 5.2 最后一步
9999166 token 过期 正常,脚本会自动重取,重跑即可
1254045 table_id 填错 重新从地址栏复制
图片下载 401 缺 drive 下载权限 补权限后重新发布版本
飞书报「出参格式不正确」 GitHub 返回 204 空响应 出参解析设为「无」,属于假报错
删了飞书记录,博客上照片还在 飞书没有「删除记录」触发器,不会通知 GitHub 先删照片再删记录,见 5.5.2

六、Vercel 部署

6.1 配置文件 vercel.json

{
  "github": { "silent": true },
  "build": {
    "env": { "HUGO_VERSION": "0.104.3" }
  }
}
  • HUGO_VERSION 锁定 Hugo 版本,必须与本地一致
  • github.silent 关闭 Vercel 在 PR 里的评论

6.2 项目设置(Vercel 控制台)

Framework Preset Hugo
Build Command sh build.sh
Output Directory public
Install Command 留空

6.3 build.sh 做什么

#!/bin/bash
set -x
hugo version        # 打印版本,便于排查

# 下载加密工具(Linux amd64)
curl -sSfL -o hugoArticleEncryptor \
  "https://github.com/zhufacai/hugoArticleEncryptor/releases/download/stable/hugoArticleEncryptor-linux-amd64"
chmod +x ./hugoArticleEncryptor

./hugoArticleEncryptor      # 执行文章加密

即:下载加密工具 → 加密 content/posts 下的文章
之后的 Hugo 构建由 Vercel 的 Hugo 框架预设自动完成(hugo --gc --minify 之类)。

这份 build.sh 与上游 exampleSite/build.sh 一致,属于官方推荐写法。

6.4 部署保护:commit 邮箱

Vercel 会校验触发部署的 HEAD commit 作者邮箱能否匹配到 GitHub 账号。

  • 本地手动 commit(邮箱 qop@live.com):正常部署
  • GitHub Actions bot 邮箱(<rand>@users.noreply.github.com):会被拦截
    报错 Deployment Blocked: the commit email ... could not be matched to a GitHub account

所以 album-sync.yml 里注入了 GIT_USER_NAME / GIT_USER_EMAIL,脚本从中读取并
git config user.email这两项不要删


七、常用命令速查

# ---- Hugo ----
hugo server --themesDir themes                    # 本地预览
hugo --themesDir themes --cleanDestinationDir     # 构建
hugo config                                       # 查看最终合并配置
hugo new posts/文章标题.md                         # 新建文章

# ---- 相册同步 ----
python scripts/sync_album.py --dry-run            # 预览(不改文件)
python scripts/sync_album.py                      # 正常同步
python scripts/sync_album.py --force              # 全量重跑(改了提取逻辑后用)
python scripts/sync_album.py --no-push            # 本地提交但不推送

# ---- 本地预览构建结果 ----
python3 -m http.server -b 0.0.0.0 -d public 1313

# ---- 加密(本地)----
./hugoArticleEncryptor                            # 加密 content/posts
Joker ·
评论 · 首页 订阅