域名是早就买好的。仓库是空的。剩下的都是我原本不太会的东西。

第一次提交里有 17 个文件,一整套 Hugo 骨架加一篇占位的 hello-world。真正让站转起来的是四份配置:hugo.toml 决定站点长什么样,wrangler.toml 决定部署到哪,.pages.yml 决定后台能改什么,.github/workflows/deploy.yml 决定怎么自动发布。写到今天,仓库里已经有 66 次提交。

这篇不写教程,就说中间那些坑。

先决定不要服务器

不想买服务器。后面所有选择都是这句话的结果。

要一台机器整天跑着,要担心它挂,要记得续费——我折腾网站是为了好玩,不是给自己找一份运维的活。所以一开始就定了静态站:Markdown 进去,HTML 出来,推上去就完事。主题挑 PaperMod,因为它干净,而且留了口子:想改哪里,在项目根放个同名文件覆盖就行。托管放 Cloudflare Workers,静态资源传上去,免费,HTTPS 自带。

推 main 分支,GitHub Actions 自动构建部署,大概一分钟。没有预览环境,没有灰度,推上去就是生产环境——这一点我用了好几天才真正有感觉。

主题其实被我改得差不多了

PaperMod 只是打底。现在页面上的东西几乎全是后加的:玻璃拟态卡片、8 张背景图每 30 秒轮换、一直飘的樱花、页脚那行北京时间时钟。真正磨人的是「看得清」。

8 张背景图深浅不一。遮罩糊厚了背景像蒙一层雾,糊薄了字就没了。我把模糊从 blur(20px) saturate(1.8) 一路砍到 blur(6px) saturate(1.4),背景立刻通透——然后实测页脚那行 12px 灰字,最透的地方对比度只剩 1.57:1。基本等于看不见。

解法不是把遮罩加回去,那背景又要糊。我给这些「直接压在背景上、底下没有卡片托着」的文字单独加了一层底衬:一层纯色,四周用模糊化开,中间是实心的,所以文字正后方的对比度可测、也稳定。页脚那行字后来回到 3.12:1。

后来才想通:文字压在纯黑到纯白的底图上,对比度上限就只有 2.86:1。光靠遮罩,物理上就到不了 3:1。

为了让音乐不断,我写了一套切页

这是整个站最花时间的一件事。

我想要的效果很朴素:点导航换页面,歌不能停。但整页跳转一定会重新加载文档,音频对象就没了。现成的库不管这个需求,它们都假设文档会被换掉。

所以自己写。拦掉站内链接,fetch 目标页,用 DOMParser 解析,然后只替换内容区那一块。页头、页脚、播放条、樱花都在内容区外面,永远不会被换掉。音频对象压根不在 DOM 里,换内容跟它没关系。

写起来才知道麻烦。最典型的 bug 是「抢跳」:连着快点几个链接,后点的会被前一个顶掉——旧请求被 abort 之后,它的 catch 还在执行整页跳转。修法是给每次导航编号,只有最新那一次允许回退。

还有一个更阴的。切到新页面时,右下角那条迷你播放条会短暂地出现两条。查了半天才明白:View Transition 里凡是声明了 view-transition-name 的元素,旧快照会作为「退场」元素一直留在屏幕上,直到整段过渡结束。把名字临时摘掉就正常了。

60 MB 的 mp3 不该躺在 git 里

一开始六首 mp3 加起来差不多 60 MB,全提交进了仓库。每拉一次代码就要拖 60 MB,而且 git 的历史是删不掉的——就算 git rm 了,历史里那一份还在。这个问题我一开始完全没想到。

后来全搬到了 Cloudflare R2,仓库里一个 mp3 都不剩。现在歌单已经扩到 12 首,仓库该多大还是多大。播放器全站只有一个 <audio>:首页大卡、音乐页大卡、右下角迷你条,是同一个音频对象的三种皮肤,切页之后重新查询 DOM 把状态画上去。

那个滑块,和一条反直觉的规矩

左下角有根竖着的滑块,访客自己拉 0 到 100,决定背景图透出多少。公式一句话:遮罩不透明度 = 100 − 清晰度。0 是全遮住,40 是默认,100 就是一点遮罩都没有。方向特别容易记反,我每次改之前都得先念一遍。

滑块用的是浏览器原生的 <input type="range">,拖动、触摸、方向键全是自带的。也没让它记住访客的选择:每次进来都是默认的 40。麻烦的还是底衬——清晰度拉得越高,遮罩越薄,底衬就得越厚。这个方向跟直觉正好相反,我差点写反。

接上后台,我就不用改代码了

接上 Pages CMS 之后,写文章不用再改文件、提交、等构建了。它不存内容,只是在仓库上做增删改,保存时替你提交一次 commit——仓库里那几次 Update content/posts/xxx.md (via Pages CMS) 就是这么来的。文章、封面图、公告、技能清单,后台里都能改。

有个细节差点把我坑了:Pages CMS 保存时会按配置里声明过的字段重新拼一遍 front matter,没声明的键会被直接丢掉。所以「后台要能改」的页面,都不能把网址和模板写进 front matter,得靠目录名去决定。

翻车记录

仓库里没有,不等于页面上没有。 技能页上我写过一句「页面里也没有任何 CDN 引入的脚本」,依据看着很充分——仓库里没有 package.json,仓库里没有 CDN 脚本。然后实测抓了线上 7 个页面:每页的 <script src> 都是 11 个,站内 10 个,外链 1 个。那一个是 Cloudflare 在代理层自动注入的统计脚本,仓库里 0 处命中,读多少遍代码都发现不了。

复核方式本身也是个坑。curl 默认发的 Accept 是 */*,而 Cloudflare 只在 Accept 含 text/html 时才注入。用裸 curl 抓,抓到的 HTML 是「干净」的,会再得出一次同样的错误结论。同一个坑我摔了两回。

wrangler 版本。 第一次自动部署就失败了,报错只有一行:The process '/usr/local/bin/npx' failed with exit code 1。原因是 wrangler-action@v3 默认装的是 wrangler 3.90.0,2024 年的版本,它不认识我配置里 compatibility_date = "2026-10-09" 这种未来日期。给 action 显式钉上 wranglerVersion: '4' 就好了。

Analytics Engine 要先在后台点一下启用。 加完访问统计,部署直接失败:You need to enable Analytics Engine. [code: 10089]。这是账号级的开关,代码里怎么写都没用。麻烦的是它失败的方式——站点不会坏,线上停在最后一个成功部署的版本照常访问;但 Actions 会一直红,我在后台写的文章也发不出去。看起来一切正常,实际上内容已经停止更新。

查询脚本八条全是坏的。 我把 SQL 的列别名写成了中文(AS 访问次数),八条查询全部报 422:Expected an identifier after AS, found: 访。而交付之前我只跑了语法检查,它是绿的。能解析,不等于能跑。

现在

访问统计上线后立刻查了一次:21 次访问,4 个访客指纹,国家 CN、来源 direct 都符合预期。里面还夹着两条波兰来的记录,是境外扫描,属于正常噪声。

从 17 个文件的骨架,到 66 次提交。域名是买的,服务器是没买的,其他都是折腾出来的。还有一堆想做的没做——音乐码率还没降,想写的东西还欠着。

慢慢来吧。