构建那一刻就定了
第一次把这站推到 GitHub,进度条停在 63%,然后失败。原因是一个 101 MB 的 mp4——单文件上限一百兆,它只超了一兆。
我把那个数字看了很久。一兆。为了省这一兆,后面花了整整一个下午。
那个视频
原片是手机直接拍的,码率高到没什么意义。ffmpeg 重压一遍,1920 宽、CRF 21、音轨直接 copy 不动,出来 75.8 MB。顺手把中间产物全删了,磁盘上收回 994 MB。
但 git 历史里那坨还躺着,.git 现在 356 MB。要彻底清掉得重写历史,把所有 commit 的 hash 全换一遍。这事我暂时不干——本地就我一个人用,等哪天真的碍事了再说。
后来换了个更省事的办法:部署用的仓库只放 out/ 的产物,234 个文件,跟源码历史完全分开。这样那 101 MB 永远压不到推送速度上。
为什么是静态站
这站没有任何需要服务端的东西。没有登录,没有数据库,没有实时数据,所有内容在构建那一刻就定了。
那就直接静态导出。output: "export"、images: { unoptimized: true }、trailingSlash: true。产物是一堆 HTML 和 CSS,丢哪儿都能跑,也不会被某个平台绑死。
代价也清楚:改一句错字,也得重新构建再推送一遍。这个代价后来变成了一个脚本,等下说。
样式为什么写成七千行
本来打算拆文件的。拆到第三次就放弃了,因为新写的选择器总要用到前面那堆变量,拆开之后来回 import,看着更乱。于是 globals.css 一路长到七千多,从头翻到底要滑好几屏。
页面级的作用域是靠 :has() 做的。比如灯箱打开的时候要隐藏竹叶和太极那一层,就写 body:has(.photo-lightbox) .leaf-layer { ... },不用给 body 加 class,也不用 JS 参与。
颜色上有个自己定的规矩:朱红只出现在「当前这一项」上,别处一律不用。这条守住了,界面就不会花。
照片墙
一开始用 CSS columns 做瀑布流。问题很快就来了:顺序是竖着排的,读起来莫名其妙;最后一行经常参差不齐;视频卡片高度算不准,会把整列顶歪。
最后改成按列高动态分配。每个卡片先量出高宽比,然后往当前最矮的那一列里塞。逻辑不难,难的是等图片加载——尺寸得写在数据里,不能等 onLoad,否则首屏会跳。
图存两套:长边 2400 的 webp 给灯箱,960 的缩略图给列表。30 个作品里有 4 个是视频,缩略图上悬停会静音预览,点开才带声音。
林间小筑
那个 3D 场景是另一个项目,打完包放进 iframe。两个 bug 折腾了一晚上。
第一个是滚轮缩放没反应。父页面把滚轮事件吃掉了,iframe 里收不到。第二个更烦:转到别的方位,一松手画面就自己转回去。
查了半天,原因是「待机摆动」——一个为了让场景别太死板加的轻微呼吸动画,它每帧都在改 controls.target.y。用户拖的时候看不出来,一松手,阻尼又把视角拉回那个被改过的目标点,看起来就像回弹。
修法是给用户操作加一个标记:一旦自己动了,就停掉待机摆动,把入场时间轴直接推到末尾,再松手就完全交给阻尼。改完那个「拽回去」的感觉才消失。
删掉十二首歌
音乐库原来有十七首,其中十二首是空的——只有条目,没有音频文件,播到就报错。
留着没有任何意义,它们只是让列表看起来更长。全删了,剩五首,每首都能播。
计数器
访问量用的是一个免费接口,不用注册,CORS 开着,hit 加一、get 读取。
有个细节必须处理:本地开发跑起来的时候,它只读不写。否则我自己刷新一百次,页面上那个数字就变成一百零几,看着像有人在看。
构建时删不掉的文件
重装依赖之后,next build 直接炸,报 EPERM,指着一个 .next/node_modules/shiki-* 的符号链接。
Windows 上这东西删不掉,也覆盖不了。后来写了个小脚本,每次构建前先把这些目录联接摘掉,再交给 Next。加进 prebuild 之后就没再见过这个错。
上线,然后被自己关掉
第一次推到 GitHub Pages,打开是一句 "Site not found"。查了半小时,最后发现是仓库不知什么时候变成了私有——免费版的 Pages 只服务公开仓库。
改成公开,重新开 Pages,好了。
还有个更蠢的:忘了 .nojekyll。Jekyll 会把下划线开头的目录忽略掉,_next 正好撞上,页面直接白屏。看到白屏那一瞬间我以为是自己 CSS 写崩了。
十分钟的缓存
改完内容 push 上去,刷新,没变。再 push,还是没变。第三次 push 完我开始怀疑推送本身失败了。
真相是 GitHub Pages 给 HTML 的 Cache-Control 是 max-age=600。十分钟。这十分钟里浏览器拿的一直是旧的那份。
知道这件事之后,我看待「部署成功」的标准就变了:不是推送成功,是十分钟后还能看到。
一条命令
后来实在受不了「构建→等→推送→等→刷新→没变」这套,写了个 ship 脚本。
现在跑 npm run ship:构建 9 到 10 秒,增量推送 3 到 5 秒,Pages 发布 39 秒左右,一共 54 秒。脚本最后会自己去线上抓一次指纹,对上了才算完。
它还会检查一件事:如果 SITE.url 还是本地地址,就先警告一句。因为我真的干过——把站推上去,结果 sitemap、canonical、robots 全指向 127.0.0.1:4321。
域名
zanecody.cn 是最后买的。买完状态是「注册局审核中」,公共 DNS 查过去是 NXDOMAIN。那会儿我以为是自己解析配错了,翻来覆去看了好几遍记录。
审核过了之后,四条 A 记录指向 GitHub 的四个 IP,www 一条 CNAME 指回去。十分钟内全部生效。
然后是 HTTPS。
到现在还没解决的那件事
https://zanecody.cn 打不开。443 端口上挂着的是 *.github.io 的通用证书,它不包含我这个域名,所以浏览器判定证书不匹配。
我试过重新提交域名、手动触发构建、把域名摘掉再挂回、查 CAA 有没有拦住 Let's Encrypt——都没有问题。想关掉 Pages 再重开强制它重签,接口回了一句 Deactivating GitHub pages for this repository is not allowed,因为 *.github.io 这种用户站不允许停用。
现在就是等。官方口径是最多 24 小时。
所以这站现在能开,但地址栏前面那行字是「不安全」。内容都在,图片都能看,只是那个提示还在。
也挺好,一篇讲建站的文章,结尾留一个没修好的 bug,比什么都真实。
