我把博客首页那张"明信片",改成了一层会换肤的纸
技术笔记 · 2026-09-27
原来的问题
风林集是我的个人博客(Astro 静态站,推 GitHub 后 Cloudflare Pages 自动部署),全站视觉走水墨路子:宣纸底色,朱砂印章 favicon(开源「敬峰中山王篆」字库矢量化),导航用毛笔楷书,背景压一层志莽行书《孙子兵法》十六字水印。这套调子调了一阵子,大体满意。
唯独首页 hero 一直没动。原是一张紫色扁平插画:星空、几何山、山顶一面小旗。它的问题不是难看——配色和造型都算精致——而是不属于这里。全站都是纸墨,就它像张玻璃贴纸,底下一条硬边,插画和正文互相不认识。
先重绘:水墨夜山
第一版方案是纯 SVG 手绘一张夜山。四层山,linearGradient 做淡墨到浓墨的过渡,下端渐隐进雾里。
画完发现第一个问题:山的轮廓太规矩。纯 path 写出来的脊线是规整折线,一眼机器活。解决靠两个滤镜配合——feTurbulence 生成一张噪声图,feDisplacementMap 拿噪声去扰动源图形的像素:
<filter id="ink-roughen">
<feTurbulence type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="3" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="20"/>
</filter>
scale 给到 20,山脊线开始抖动、歪斜,有笔意了。不敢再大,再大整座山就散架。
第二个问题出在飞鸟上,而且是方向性的:我先画了墨色的鸟,结果在深色山体剪影上根本看不见。夜山里的鸟不该是落墨,该是留白——深底上只有浅色能立住。这个弯转过来之后,其余都顺了:孤松画一棵,<use> 复用;浅色档、深色档切过去看都成立。
这一版发出去,收到的意见是:方向不对。原话是——
“hero 不该是一幅画,该是一层皮。”
结构问题:画和皮的差别
这句话需要翻译。
全站纸墨主题的世界观是”一张宣纸”:字写在纸上,章盖在纸上,水印压在纸上。而 hero 是一张焊死在顶部的夜景图,画得再像水墨,性质上仍是贴上去的明信片——切到纸墨档它还是那张夜,被硬塞进了另一个世界观。
所以要改的不是画法,是结构:构图只留一份,所有填色抽成 CSS 变量 --hero-*,随 data-theme 换肤。夜山档下它是夜空浓墨,纸墨档下它是宣纸天光。
实现里有个不翻资料很难发现的坑:<linearGradient> 的 <stop> 元素不能直接在属性里挂 var(),<stop stop-color="var(--hero-sky1)"> 是无效写法。必须给 stop 挂 class,在 CSS 里控制:
.hero-stop-1 { stop-color: var(--hero-sky1); }
stop-opacity 同理。这个细节后面有直接用场。
换肤很快碰到边界情况:纸墨档要不要星空和月亮?结论是不要。这不是配色问题,是世界观问题——宣纸上没有星空。所以 .hero-moon 和 .hero-stars 在纸墨档下隐藏,换一轮朱砂日显示,机制和站内主题图标同一套,display 切换。
插曲:一个线上 bug
改主题配置时翻出一个跟 hero 无关、但在线上活了一阵的 bug,值得单独记。
BaseLayout.astro 里有段防闪烁的内联脚本:加载时读 localStorage 里的主题值,赶在样式表生效前把 data-theme 打到根元素上,否则每次硬刷新页面都会先闪一下默认主题。脚本里维护了一个合法值白名单,内容是 light 和 dark——paper 不在其中。
后果:切到纸墨档,站内跳转一切正常;一旦硬刷新,主题被打回浅色。paper 只在站内 view-transition 跳转时存活。
修复一行:白名单加 paper。这类问题的共性是同一份知识(“一共有哪几档主题”)被写进了不止一个文件,而没有任何机制保证同步。加档位时漏掉的地方,往往是所有用户进站的第一个入口。
隔纸透字
hero 换好皮之后来了个新需求,原话:“透过图片也能看见它背后的字,隐隐约约、朦朦胧胧的美感。”
那个”字”,是全站固定定位压底的十六字行书水印:right: 4vw、通高 88vh、opacity: 0.13。之前被 hero 不透明的天空整个挡住,只有滚到正文才见得着。
方案就是前面埋的 stop-opacity:天空渐变的 stop 挂上透明度,天光变成半透明的一层纸,水印从山的背后透出来。
第一版透明度给到 0.72~0.85,反馈是”跟没改一样”。第二版加强到 0.58 / 0.52 / 0.46 / 0.75(自上而下):最透的一段,将近一半的不透明度让给了水印,行书笔锋在云层下清晰可辨。山体保持完全不透明——字从天光里走,浓墨处压住。全透出去不叫朦胧,叫没画完。
最后一处是底边。山的浓墨在某个像素上戛然而止,下面接正文宣纸,仍是一条硬边。加一个只在纸墨档显示的 <rect>(y="470",高 170),填充渐变到宣纸色 #f3edde,浓墨山脚化进正文。至此 hero 的四条边全部处理完,和页面连成一张纸。
减法:三档砍两档
上线前又做了一次减法:删掉浅色档,全站只留 dark / paper 两档。
- 切换循环
['dark', 'paper'],切换钮只留月亮和卷轴两个图标; - localStorage 里遗留的
theme=light一律归入paper; - 新访客跟随系统:
prefers-color-scheme: dark进夜山,其余进纸墨——纸墨是门面; - 浅色档的 CSS 变量块保留作基线。死代码无害,删挂了才是事故。
其他几个坑
- git 分叉:此前图省事走 GitHub Contents/Git Data API 推代码,本地 main 和远程历史谱系分了叉。理平流程:
git branch backup-main备份 →git fetch <token-url> main→git reset FETCH_HEAD(mixed,工作区不动)→ 以工作区为准重新提交 → 恢复普通git push。API 推送这条路封了。 - 僵尸 dev server:4321/4322 被往期会话的
astro dev占着,新服务挤到 4323,对着旧端口看”改了没生效”,全是错觉。开工先lsof -nP -iTCP -sTCP:LISTEN | grep node。 - push 间歇 502:
CONNECT tunnel failed, response 502,代理抽风。45 秒退避重试,本次两次都是第一次重试即过;git config http.version HTTP/1.1兜底。 - 验证线上 CSS 别 grep 带空格的串:构建产物压缩掉了空格,
stop-opacity: var(...)早成了stop-opacity:var(...)。搜不到先想想产物的形态,别急着怀疑改动没生效。 - 本机没 Chrome:playwright-cli 用
--browser=webkit。另,刚起的服务器可能未就绪,“Could not connect” 等三四秒重试,不是代码的问题。
收尾
相关提交:7b471eb 重绘水墨夜山;93abf11 一套骨架几张皮,顺手修白名单 bug;effde06 两档制、透字加强、化墨收边。代码在 huo241/fenglinji,改动集中在 src/components/Hero.astro、src/styles/global.css、src/layouts/BaseLayout.astro 三个文件。
回头看,这次改造里真正起作用的不是哪个滤镜、哪个变量,而是先把 hero 的身份想清楚:它是页面的组成部分,还是页面上的装饰品。这个问题定了,后面的技术方案其实都不难选。
评论