之前我做了一个hugo静态主题,感觉用着挺麻烦的,每次改点东西都要重新推一次。还好之前加了简单的文章管理支持,增加sveltia cms后更新文章不用每次都推一次了,但改一些文章之外的东西还是要重新推,加上之前说的waline评论有时抽风的问题,这次直接一起都改了,按我的十月计划重新做了个后台,当然我只写了一点,大部分让AI做的。
这次的主题是 zero-theme,接口和后台在一个 monorepo 里叫 zero-stack,两个都是 Apache-2.0。
简介
**网站还是 Hugo 构建的纯静态 HTML,动态的那部分是一套跑在 Cloudflare 免费档上的后端。**
数据分两处:文章、时刻、导航、页脚这些"站长的东西"存在自己的 Git 仓库里,由 Hugo 构建;评论、点赞收藏、订阅、访问统计、账号这些"读者的东西"存在 Cloudflare 的 D1 里。中间靠一个 Worker 接口连着,后台改一处就提交一次仓库、触发一次重建。
这次不是四五个第三方服务拼在一起,是一个后台管全部,我把后台做的尽量可以控制全部常用设置,所以构建次数可能会多一些,有些设置点一次就会构建一次。
在线演示
演示站点: blog.molyun.com
主题仓库:https://github.com/AUkarl/zero-theme
后端仓库:https://github.com/AUkarl/zero-stack(接口 + 后台 + 评论组件)
只想看看主题长什么样:进 zero-theme 跑 `npm run dev`,它自带一份示例站点,不用碰后端
特性总览
前端(zero-theme):
**零框架** —— Hugo 模板 + 原生 CSS 变量 + 手写 JS,没有 Tailwind、没有 jQuery、没有打包器,构建就是 `hugo –minify`
**落地页 + 两种首页布局** —— Bento 精选网格 / 旧版等高卡片,后台「前端管理 → 首页」二选一
**图片管线** —— 站内图统一出 400/800/1200 三档 WebP,另出一张 20px 模糊小图内联当占位,进视口前 200px 才换真图
**不引外部字体** —— 只用系统字体栈,Font Awesome 打进仓库(css 加四个 woff2,一共 382KB),页面不请求任何字体域名
**数学公式按需** —— front matter 写 `math: true` 才加载;没写就构建期扫一遍正文,真出现公式才加载
**18 个短代码** —— 音频五个(网易云、QQ 音乐、酷狗、通用),视频七个(B 站、优酷、腾讯视频、YouTube、通用),剩下六个是画廊、图片说明、引用、代码块、公告、KaTeX
**暗色三态** —— auto / light / dark,跟随系统也跟随手动,评论组件跟着一起换色
**多端适配** —— 17 个 CSS 文件里 43 个 `@media` 断点,手机上从头到尾能用
**自带访问统计** —— `sendBeacon` 打自己的接口,不引第三方脚本
后端(zero-stack):
**一个 Worker** —— Hono + Drizzle + Zod,公开接口和管理接口在同一个包里
**内容留在 Git** —— 后台发文章、发时刻、审核友链、通过作者认证,最后一步都是往你自己的私有仓库提交一次
**D1 23 张表** —— 账号、评论、点赞收藏、订阅、群发、友链、统计、配置、投稿、认证、审计
**R2 两个桶** —— 私有桶只让 Worker 读,公开桶绑自定义域名出直链
**Queue 发邮件** —— 出站邮件走队列异步发,失败最多重试三次,每封都留记录
**Cron 每天一次** —— UTC 02:00(北京时间上午十点)一次跑完十件事,改动了仓库文件只请求一次重建
**评论系统可换** —— 后台切 waline / twikoo / giscus / artalk,前台不重新构建就换过来
**反垃圾在服务端** —— 内置一套正则规则,能加关键词、能加自定义正则、能接外部审核服务商
**全站备份与恢复** —— 一份 JSON 十一个分区,恢复前先跑演习,图片本体单独打包搬
**后台 20 个页面** —— 非管理员访问这些地址,直接给你一个 404
前端页面
落地页
落地页就是打开先看到一句话,再看到文章。我自己放的是"Hey,好久不见",上面一行诗词,下面一个进入按钮。不想要这一页的话,后台把 `/` 直接让给列表就行。
Home页
首页我做成 Bento 格子:精选文章、置顶、统计条、封面卡混排。另一种是旧版等高卡片,后台「前端管理 → 首页」二选一,改完点保存,仓库里多一个提交,Pages 重建一次。
文章列表顶上有最早 / 最新 / 最热三个筛选,精选是按评论、点赞、收藏、阅读量排出来的前若干篇,设了置顶的优先。
文章页
文章页头部是分类胶囊、标题、发布日期、分类、字数、阅读时间、作者,右边一列目录。目录可以在后台按篇关掉,关掉那一篇正文就居中显示,不占右侧那条。
时刻页
时刻页
时刻就是网页版朋友圈:正文、九宫格图、一个音频或一个视频、一个链接卡片,日期可以随便设。注意时刻的正文保留了短代码支持,实际是可以发布文章那种长文的,但我建议不要,样式感觉比较丑。
友链页
友链页
四个 tab。朋友动态是定时任务抓别人 RSS 攒出来的一页;左邻右舍和站点联盟是两份名单,读的是仓库里的 `data/links.yaml`;申请友链是在线表单。
「迷失的小船」按两条规则算:30 天访问不通,或者 90 天没更新(只看填了 RSS 的站),两个阈值都能在后台调。
申请表单固定七个字段:站点名称、站点地址、站长邮箱必填,RSS、头像、friendLink、简介选填,简介独占最后一行。表单里埋了一个蜜罐字段,脚本填了它就直接丢弃但还是回你"已提交"。
归档、关于、工坊、订阅、账号、404
图一 归档
图二 关于
图三 工坊
图四 订阅
图五 账号
图六 404
归档页左边一条时间轴跟着滚动走,中间按年月分组,右边是搜索框、分类和标签。搜索是客户端过滤,标题分类标签命中就显示——我没有为了搜索去引 Algolia 或者 Fuse.js 再塞一个索引文件给每个访客。
关于页四块:关于我、关于本站、站点声明、找到我,每块的条目都能在后台增删。
工坊页是项目卡片,名称、简介、状态、跳转链接,后台可增删排序。
订阅页、导航里的订阅入口、文章页那个订阅框,是同一个开关管的。后台关掉 `subscribeEnabled`,这三处一起消失,`/subscribe/` 这个地址也只回一句"订阅功能已关闭"。这张图就是关着的状态。
账号页可以登录、注册、找回密码。我新增了用户注册的能力,用户注册后可以查看编辑自己的评论、点赞、收藏、改密码、两步认证等基础功能,我还新增了作者申请,管理员在后台审核后可以开启用户投稿权限,用户在个人中心投稿经管理员审核后可以直接发布,但我觉得这个功能还是不开为好。
404 页还是老样子,宝贝回家+徐志摩的《偶然》
评论组件:
评论组件
`` 是一个自定义元素,Hugo 页面里写一行标签就完事。它管评论列表、发表框、点赞、收藏、订阅框、时刻卡片和登录入口,是 zero-stack 里用 Svelte 编译出来的一个包,123KB,gzip 之后 44KB,直接提交进主题仓库。
它也不在每页硬加载。页面上有个 3.6KB 的探针脚本,等真有一枚组件进到视野附近 400px 以内才去下载那个包。落地页、关于页、归档页、404 页上一枚组件都没有,那 44KB 就根本不请求。
图里这几条是我用游客身份发的演示评论。几个简单说明的点:
昵称留空会按邮箱生成掩码,`zhangsan@qq.com` 显示成 `z***@qq.com`;游客邮箱必填,昵称和网址选填。
头像按邮箱来:QQ / Foxmail 且 @ 前是纯数字走 QQ 头像,其余走 Cravatar(Gravatar 的国内镜像),图挂了退回 Gravatar,再退回名字首字母色块,多次查询有点慢。开启用户注册后,用户登录可以自定义昵称和头像。
时间,属地,浏览器,系统,正文Markdown,楼中楼平铺回复,工具条短链快捷键和表情我都加上了。
匿名访客提交成功后,浏览器本地记下这条评论的自删凭证 ownerKey,自己的待审评论会显示"审核中"并留在列表里,还能就地编辑和删除。换浏览器或者清掉本地存储就删不了。
评论系统可以随时切回 Waline / Twikoo / Giscus / Artalk,后台改一下 provider,前台不用重新构建就换过来。切换的通道我实测过:构建期写成 waline,后台切成 zero,前台自己换过来了。但这四家我并没有各自部署一套去验最终渲染效果,得你自己试。
后台管理
zero-cms 是 SvelteKit + Svelte 5 写的单页应用,部署在 Cloudflare Pages。
**不是管理员登录进来,这些页面全部返回 404**,不是跳登录页。跳过去就等于告诉别人"这里有个后台,只是你没权限"。
图一 登陆页
图二 管理面板
图二 前端管理
可以看到后台功能很多,我不一一截图了,截图也很慢。
文章编辑器左边是仓库目录树(`content/zh/posts` 一层层点开),右边一列结构化字段:标题、副标题、发布时间、修改时间、摘要、描述、分类、标签(都是回车添加的胶囊)、封面图(能直接从媒体库挑)、草稿、置顶、weight、目录 toc、公式 math、作者标识。下面才是 Markdown 正文框,工具条是粗体 / 斜体 / 行内代码 / 代码块 / 链接 / 站内短链 / 标题 / 引用 / 列表,旁边一个短代码下拉加「插入媒体」,键名和参数取自主题 `layouts/shortcodes` 目录。页面顶上还有一条「发布后群发订阅」的勾选。
保存分两步,这个交互我用得最多:**编辑器里的「保存本地」只写进浏览器缓存**(切文章、关后台都不丢),**页面右上角常驻的「保存到 Git」才真提交**,一次把攒着的几篇整批推上去(一次构建还有点问题,有时候会一个一个构建)。
草稿是真的草稿:勾了草稿的文章照样提交进仓库,但 `hugo` 不带 `-D` 不渲染它,前台列表、RSS、sitemap 里都没有。
时刻那一页和文章编辑差不多。
媒体库支持绑多个存储桶,上传前先在浏览器里过一遍 canvas 转成 WebP,质量 0.85——这个档位对照片和截图都够用,再低就开始出块状噪点。GIF 和 SVG 不动;转出来不比原文件小(或者浏览器不认 WebP、“成功"返回一张 PNG)也原样传,不然地址和真实类型对不上,主题的图片管线会认不出这张图。有一条硬规矩:**写进 Markdown 的图片地址必须是绝对地址**。站点和 API 不同源,相对路径 `/api/media/…` 到了站点那一侧就是站点的 404,表现是"后台填的封面和正文图片前台都不显示”。
评论管理是四个状态标签:待审核 / 已通过 / 垃圾 / 全部,待审那个数会出现在侧栏。每行给的是昵称、邮箱原文、层级(第几层)、状态、它挂在哪篇文章上,操作只有两个:标为垃圾、删除。邮箱只有在这一页看得见,前台一律是掩码。审核规则在服务端:内置一套开箱正则(广告、违法、色情、赌博那些),命中阈值 60 直接进待审而不是被删——宁可进待审也不误杀作者。可以追加关键词(支持 `/正则/flags` 写法)、编辑自定义正则、配外部审核服务商(网易易盾、阿里绿网、腾讯天御这些),下面那个试跑框粘贴一段文本就能看到得分、命中规则和返回。
规则文件必须放在仓库的 `.zero/spam-patterns.txt`。这个我踩过坑:放错地方 Hugo 会把它当数据文件解析,整站构建直接失败。
友链和友链申请合并成一页两个 tab。通过、驳回都会把结果和理由发到申请时填的邮箱。友链的新增、修改、换组、删除都会同步仓库里那份 `data/links.yaml`——前台那两个板块是构建期读 yaml 渲染的,只改数据库就会出现"库里删了、页面上还在"。分组只有一处真值:`kind=friend` 进 `friends:` 段,前台就是左邻右舍;`kind=alliance` 进 `alliance:` 段,前台就是站点联盟。
用户和邀请码也是一页两个 tab。用户组只有「朋友」和「共创」两种,作者认证通过自动进共创。管理员只有一个,就是部署时注册的第一个账号,不可注销、不可删除、权限不可转移——这一页会把他那一行列出来,标着「站长」、三个权限位常亮置灰,只有作者标识可以改,其余操作对他是灰的。封禁开关旁边是「删除账号」,要手输邮箱确认才会连同全部数据删掉。
投稿审核那里可以直接编辑投稿内容再放行,通过后自动发一封审稿通过邮件,并把投稿人的资料 upsert 进 `data/authors.yaml`。
订阅名单每行有「直接确认」「取订」「删除」三个操作。群发的「关联文章」是已发布文章的下拉,按语言分组,选中就带上 postId。每日发信上限默认 100,额度不够这次收件人数时**整批不发**,任务留在队列里等第二天 Cron 自动续跑——发到一半被服务商拒掉、重跑时又把收到过的人再发一遍,比整批延后更糟。
前端设置页:**这是我最想让你看的一页。** 基本信息 / 导航栏 / 首页 / 页脚 / 关于页 / 工坊 / 时刻页 / 友链申请 / SEO 九个面板,共用页面底部**一个**「保存设置」按钮。每个标签上带一枚状态章:这一段已经写进 JSON 了,还是仍然回落 `params.toml`。
它写的是内容仓库里的 `data/*.json`,主题在构建期读这些文件。所以我在后台改一句页脚文案,点保存,仓库里多一个提交,Pages 重建一次,前台就变了——全程不用碰 `params.toml`,也不用懂 Hugo。九个面板共用一个按钮是故意的,以前每个面板一个按钮,我一次改三样东西能触发三次构建。
最后一个标签「主题配置文件」这个是直接读仓库里那份 `config/_default/params.toml` 原文,整份文本原样写回 Git,注释、空行、顺序都不重排。它有自己的读取与保存按钮,不走上面那个统一保存。想改一个面板里没有的项,就从主题那份默认配置里把键名连同所在那一段抄过来,不必整份复制。
设置里那些开关是**运行期**的(`/api/config`,30 秒内生效,不用重建站点):订阅、友链、时刻、注册、收藏、点赞、评论属地、账号系统、评论审核……关掉之后前端对应入口整块消失,对应页面只回一句"该功能已关闭"。这一页分成十三个可折叠的组,同一个配置键只留一个入口,重复的都已经删掉了——早先「我的信息」里有登录方式、「设置」里也有登录安全,「前端管理」能配时刻、「设置」里也有一份时刻,这种留着就是给自己找不痛快。
发信方式按通道分节显示,只显示当前选中的那条通道,没选中的字段既不占位也不会被改动;SMTP 那一节底部是常用服务商按钮,点一下填好服务器和端口。密码 / 授权码留空即不改已存值,也可以用 wrangler secret 配,两处都有值时以这里填的为准。
邮件模板四种:验证码、订阅、评论通知、通用通知,正文是 HTML 源码,上面列着能用的占位符,改完可以在测试发送那一节直接看效果。所有发出去的信都进 `email_logs`,包含邮件服务器回的原话——这个救过我,之前一直"发不出去",看了原文才发现是 465 端口要走隐式 TLS。
翻译工作台:这个功能是多出来的,当前没什么用,这个主题本来是要做多语言的,但是让AI跑坏了,我一气之下直接全删了,后续可能再加回来,这个功能就没删。
备份只有一种:全站。一份 JSON 带十一个分区——站点设置、规则、主题样式、文章、时刻、友链、订阅名单、评论、点赞收藏、普通用户、媒体清单。恢复时**逐区勾选 + 先跑演习**,演习一个字都不写,只报每区会写多少行、哪些文件同名、配置将改哪几项,看过演习结果「推送恢复」才会亮。真写是按区分批提交的,每批五个文件。
图片本体不在那份 JSON 里,同一页下面有「导出图片包」,到新站那一节逐张写入,按**原对象键**落回,本站已有同名的自动跳过——所以文章里已经引用的图片地址一个字都不用改。
密钥和"本站专属"的配置永远不导出也不写回新站(站点地址、仓库归属、发信凭据、各家 API Key 一共十七项),只在文件的 `omitted` 里列出旧值供人工对照。也就是说新站的域名、仓库、发信凭据这三样必须自己重配一遍。
管理员这一页管登录方式:单密码、密码 + 身份验证器、GitHub 授权、密码 + 邮箱验证码四种,全部可用。**这条策略只约束后台管理员**,普通用户在前台永远是邮箱加密码或者选两步验证。
每一次写入后台的接口都留一条审计,谁在什么时候改了哪个配置、推了哪个文件。
配置到底以哪份为准
这块最容易把人绕晕,写成两层:
```
后台「设置」写进 D1 的 site_config —— 运行期开关,30 秒生效
后台「前端管理」写进仓库的 data/*.json —— 构建期外观,要重建
```
主题自己那边又是三层,取值永远是:
```
站点 data/*.json > 站点 params.toml > 主题默认 params.toml
```
所以**导航、页脚、首页改不动的时候,八成是你去改了 params,而它已经被 data 覆盖了**。后台对应子标签上那个「删除覆盖,回落 params.toml」按钮,就是删掉那个 JSON 文件。主题默认的 `params.toml` 620 行、24 段,每段都带注释,想改一个站点文件里没出现的项,从主题那份把键名连同所在那一段抄过去就行,不必整份复制。
部署要装什么、要多久
具体步骤两个仓库的 README 和 zero-stack 的 `docs/DEPLOYMENT.md` 里都有(含 GitHub Actions 模板),这里简单提一下。
要装:Node.js ≥ 22、pnpm ≥ 11、Hugo extended(0.165.0 已验证,必须带 `+extended`,主题用了图片处理)、一个 Cloudflare 账号(免费档就够)、一个私有的内容仓库。
后端侧一条向导 `pnpm setup`,常规九问,配 SMTP 加"回写 Git"会到十七问。它建 D1、两个 R2 桶、两个 KV、一个队列,跑远程迁移,部署 Worker 和后台;密钥只通过标准输入写,不会拼进命令行也不会落进任何文件。然后 `pnpm create:site` 在你的仓库同级目录生成内容仓库骨架。
最后说两句
主题我已经适配了手机、平板、PC端,手机上也可以轻松使用。
这套后端是为我的主题定制的,它不通用,有觉得有用的可以借鉴我的,但不建议原样搬,我感觉写的不咋地。两个仓库都在 GitHub 上,README 里有完整的安装、配置、部署和故障排查。有 bug 或者觉得哪里别扭,开个 issue 说一声,我不一定马上改,但会看,会记着。
这套新的后端可能有点乱,但功能我认为已经很全了,欢迎大家帮我测试一下,大家发现什么问题或有什么好的想法欢迎提issue,我接着改。
本文章部分内容经 AI 修改