适用版本:Hugo
0.104.3(extended)+ 自定义主题writing
最后更新:2026-09-10
目录
一、项目概览
技术栈
| 项 | 值 |
|---|---|
| 静态生成器 | 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.htmlshortcode 与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 创建飞书自建应用
- 打开 https://open.feishu.cn/app → 「创建企业自建应用」
- 「凭证与基础信息」里复制 App ID 和 App Secret
- 「权限管理」里开通:
- 多维表格类:
bitable:app:readonly(脚本只读,够用)或bitable:app - 云文档 / drive 类:带「下载」字样的必须勾(否则拉附件 401)
- 多维表格类:
- 「版本管理与发布」→ 创建版本 → 申请发布(个人应用一般秒过)
权限点完开通后,不发版本不生效
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
仓库 → Settings → Secrets and variables → Actions → 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 请求
- Method:
POST - URL:
https://api.github.com/repos/zhufacai/Blog/dispatches - Headers:逐个添加,Key 与 Value 分开填
| Key | Value(只填这一列的内容,别带 Key 名) |
|---|---|
Authorization |
Bearer 你的PAT(Bearer 后一个空格,再贴 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_list带pasteUpdate / 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 三档文件,
末尾还有一轮孤儿文件兜底清理。所以只要能跑一次同步,删除就会生效——
问题只在「没人通知它跑」。
推荐做法(零改动):先删照片,后删记录
- 打开那一行,把「照片」字段里的附件删掉(或整列清空)
→ 「照片」字段发生变化 → 命中第 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