Featured image of post 给博客换一张脸:薄暮花海、拉链小虫和一场安静的搬家

给博客换一张脸:薄暮花海、拉链小虫和一场安静的搬家

这几天把博客从里到外翻新了一遍,记录一下改了什么、为什么这么改、踩了哪些坑

博客是从十月初开始陆陆续续改的,最近几天集中做了一次大翻新:从"主题默认皮"改成了现在这个样子。这篇不写教程,只记录一下改了什么、为什么这么改、踩了哪些坑,顺便把藏在角落里的几个小彩蛋交代清楚。


🎨 先定一个调子:薄暮花海

一开始的想法很朴素——“背景好看点就行”。试了几版都不太对:纯色太素,照片平铺又太吵。最后定下来的方向叫「薄暮花海」:

  • 薄暮:深靛蓝 → 雾紫 → 雾粉的渐变夜空,叠一层缓慢漂移的柔光光斑
  • 花海:飘落的花瓣 / 叶片,以及首页那条"走到哪算哪"的花海进度

关键原则是不额外请求任何图片:渐变、光斑、花瓣全是 CSS 和 SVG 画的,首屏几乎零成本。

1
2
3
/* 背景是两层伪元素:底层渐变 + 上层极光 */
body::before { background: linear-gradient(...); }
body::after  { background: radial-gradient(...); animation: drift 40s ease-in-out infinite; }

配上玻璃拟态卡片(半透明 + backdrop-filter + 细描边 + 柔和投影),标题换成衬线体(宋体系),正文保持黑体——文学感和可读性各退一步,刚好。


🧭 导航:从左侧栏搬到顶部胶囊

主题原本是左侧栏 + 右侧小部件。我把它换成了顶部胶囊导航,两侧小部件全部收进导航和页脚。

这里有个小细节:主题的 JS 依赖 #toggle-menu、#main-menu、#dark-mode-toggle 这三个节点,覆盖布局时要把它们保留下来,否则汉堡菜单和明暗切换会莫名其妙失效——这大概是改主题时最容易翻车的地方。

首页做了全屏 Hero:

  • 照片直接当整页背景(data-src 懒加载 + 预加载下一张)
  • 居中站名「huanhe」,配一行手写体
  • 语录逐字打出、逐字退格,像打字机一样
  • 每次进首页随机选一张照片、随机挑一条语录,所以刷新几次每次都不一样
  • 往下滚动,背景会渐进模糊,把视线让给正文

文章列表改成了 3 / 2 / 1 列的响应式网格,卡片高度自动对齐。


🌸 季节主题:春天和秋天不是一回事

给整站做了一套季节配色:按月份自动切换(3–5 春、6–8 夏、9–11 秋,其余冬),也支持用 ?season=spring 这样临时预览。

做法是把颜色全部收进 CSS 变量,一处切换全站生效:

1
2
html[data-season="spring"] { --season-accent: #d9558f; --season-bg-top: #2a1f3d; ... }
html[data-season="autumn"] { --season-accent: #c4691f; --season-bg-top: #2b1d2c; ... }

飘落装饰也跟着季节换:春樱瓣 / 夏绿叶是 SVG 剪影(同系三种颜色随机),秋枫叶 🍂 / 冬雪花 ❄️ 直接用 emoji。

这里有个我自己推翻的决定:一开始手机上直接关掉了飘落效果,怕卡。后来在手机上刷自己博客,觉得少了点意思,就改成"少而小"——桌面 34 片,手机 14 片、尺寸打八折,跨断点还会自动重建。

另外手机浏览器的地址栏会收放,100vh 并不可靠,所以高度用了 vh 和 dvh 双写,支持 dvh 的浏览器自动用更准的那套。


🗺 花海进度:9 / ∞ 个地方

首页有一张卡片,记录我走过的地方:临沂、图书馆、南大、海尔、幂律、洛阳、西安、北京、洛杉矶。

数字不是写死的,来自 data/places.yaml:

1
2
3
4
5
- name: 临沂
  note: 家
  article: ""          # 有文章就填目录名,可以点击跳转
  x: 9
  y: 72

显示成「9 / ∞ 个地方」——分母是无穷大,因为地方是走不完的。已点亮的地方在进度条上开出一朵朵小花,整条光带是流动的,永远走不到头,倒也挺符合"去看花海的路上"。

花海专题页现在是空的,只写了一行「待开发」。原因是我对两版自动生成的地图都不满意——这一页我想自己画。地图画好之后,把 places.yaml 里的坐标点上去就行。


🐛 小彩蛋:一条会漏虫子的拉链

导航框下方挂了一条拉链和一个小挂件。左右拖动挂件,拉过的地方链齿会岔开,然后……漏出小虫子(三只 Q 版小家伙随机一只,掉到屏幕底下就自己消失)。

快速拖动的时候会有个坑:

  • 第一版用"绝对位置“记账——快拉时一次跳过一大段,中间那段既没漏虫、也永远不会再漏,表现就是"漏一半就停”
  • 而且滴漏是靠 requestAnimationFrame 驱动的,在没有渲染帧的环境里干脆一动不动

后来改成了三个东西配合:

  1. 按拉开距离记账(每 26px 一只),快速大步拖动也不会漏掉距离
  2. 令牌桶限速,突发有上限、持续有速率,性能可控
  3. 定时器滴漏,松手之后继续把这段拉链漏完
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 只留待漏距离,慢慢滴完
function drainTick() {
    refillTokens(performance.now());
    while (bugBalance >= BUG_STEP && tokens >= 1) {
        if (!spawnBug()) break;
        bugBalance -= BUG_STEP;
        tokens -= 1;
    }
    scheduleDrain();
}

顺手加了个互动:点一下虫子就能把它按掉(冻结在当前下落位置 → 缩小淡出)。


🏷 手绘风:贴纸标签和框格

导航框下面粘了四个"贴纸":GitHub、哔哩哔哩、邮箱、微信。手绘描边 + 每张略微倾斜,鼠标移上去会"扶正"一点。邮箱和微信点一下就是复制,不用手动选中。

配合的还有一套「手绘框格」样式:纸张质感底 + 不规则圆角 + 双层描边,用在评论区和一些卡片上,跟夜景渐变意外地搭。

手机上这四个贴纸会挤,所以窄屏(≤560px)只留图标,去掉文字标签就舒服了。


🎵 播放器:从"资源加载失败"到自己的播放条

原来用的是网易云的外链 iframe 播放器,图省事。结果经常弹一句:

资源加载失败

排查下来是第三方 iframe 在跨站环境里自己加载不了资源。于是干脆自己做了一个播放条,只借它的外链音频地址:

1
audio.src = 'https://music.163.com/song/media/outer/url?id=' + song.id + '.mp3';

现在的播放器:▶ / ⏸、进度条点哪跳哪、上一首 / 下一首、播放时 ♪ 音符从按钮里跳出来;整页刷新会接着上次的位置播。

歌单也不再写死了。构建时用脚本抓一次公开歌单,只保留真的能播的歌:

1
2
3
4
def playable(song_id):
    """外链音频地址是否真的返回音频(VIP / 下架会跳到 404 页面)"""
    ...
    return "audio" in ctype

VIP 歌曲匿名播不了,脚本会自动跳过——所以歌单里加 VIP 歌是看不到的,不是坏了。

踩到最深的坑:<script> 里 Hugo 会自动把字符串渲染成合法的 JS 字面量,我多套了一层 jsonify,结果歌曲 ID 变成了带引号的字符串:

1
2
请求地址:?id=%222724791369%22.mp3   ← 引号被转义进去了
网易返回:MEDIA_ELEMENT_ERROR: Format error

去掉那层 jsonify 就好了。这个坑很隐蔽——页面看起来一切正常,只有播放器默默不响。

切页面音乐不能断

旧方案还有个致命问题:点一下链接整页重载,播放器被销毁,音乐戛然而止。

解决办法是把站内跳转改成 PJAX:只替换内容区,导航(以及导航里的播放器)原地不动。

1
2
3
4
5
6
7
fetch(url.href).then(r => r.text()).then(html => {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    root.innerHTML = doc.getElementById('pjax-root').innerHTML;
    document.body.className = doc.body.className;   // 每页的样式靠 body 类名区分
    runScripts(root);                               // innerHTML 插入的脚本不会自动执行
    history.pushState({ pjax: 1 }, '', url.href);
});

换页后要顺手同步标题、body 类名、菜单高亮,还要把新内容里的 <script> 重新执行一遍。

⚠️ 代价是页面脚本必须幂等:反复进出首页不能叠加定时器和监听器。我的处理是给首页脚本发一个 token,旧循环自己退出;花瓣层重建前先清掉旧的;“回到顶部"改成事件委托 + 只注册一次。要是哪个链接不想走 PJAX,加个 data-no-pjax 就行。


💬 评论区和一点隐私

评论用的是 giscus(基于 GitHub Discussions),外面套了玻璃卡片和一句「留下一句话」的衬线标题。评论框的配色也自己托管了两套 CSS,跟着站点主题走,不再依赖第三方的默认皮肤。

邮箱和微信号我做了个防爬处理:不在源码里写明文,改成 Base64 存放,点击复制的那一刻才解码。

1
2
let raw = btn.getAttribute('data-copy');
try { raw = window.atob(raw); } catch (e) { /* 兼容旧写法 */ }

说白了这只能挡住"扫源码的笨爬虫”,防不了有心人——但聊胜于无,至少不会第二天就开始收垃圾邮件。


⚡ 性能:把"好看"的代价压下去

动效和图片是最容易把首屏拖垮的两样东西,所以专门收拾了一轮:

项目之前现在
首页照片JPGWebP 1600×900,体积小 54%
角落里的吉祥物PNG 237KBWebP 17.8KB(省 93%)
首页 HTML—gzip 后约 12KB
静态资源缓存默认图片 30 天、带 hash 的 JS/CSS 一年 immutable
服务器传输默认开了 gzip(压缩级别 5,兼顾 CPU)

另外所有动效只用 transform 和 opacity,并且尊重系统的「减少动态效果」设置——开了这个选项,动画会安静下来,只剩淡淡的状态切换。


🚀 部署:双击一下就完事

部署链路是 push → GitHub Actions 构建 → rsync 同步到服务器,本地只留一个 publish.bat(add / commit / push),不用记命令。

有个小机关:如果本地没有任何改动,它会自动建一个空提交,用来"纯刷新"站点。因为歌单是构建时抓的,我在网易云加了歌,本地文件其实一个字节都没变——没有这个空提交,git push 只会回一句 Everything up-to-date,什么都不会发生。

另外挂了个定时任务每 2 小时重新构建一次,顺手把歌单刷新了,不想等就手动双击一次。

服务器这边也在搬家的路上(换机房,备案接入审核中)。搬家期间老机器继续服务,域名不解析到新机器——这是备案的硬规矩,急不得。


📝 写在最后

这一轮改下来最大的体会是:静态博客也能做出"活着"的感觉。

不需要后端,光靠 CSS 变量、transform 动画、一点点原生 JS,就能有四季、有随机、有能玩的拉链、有不间断的音乐。代价是每个动效都要想清楚"它在低端机上要花多少",这也是我中途推翻好几个效果的原因——好看是加分项,卡顿是减分项,而后者的权重更高。

接下来想做两件事:

  1. 把花海地图自己画出来,让那 9 个地方真正落在图上
  2. 继续补细节:Hero 文字的对比度、分享卡片、响应式图片

改站如改人,慢慢来。


本站用的还是 Hugo + Theme Stack,所有改动都在站点级覆盖里,没动主题源码——这样以后升级主题不会把这一堆自定义全冲掉。

愿我们都在去看花海的路上 🌸
使用 Hugo 构建
主题 Stack 由 Jimmy 设计