2726 字
14 分钟
– 次浏览
– 位访客
给博客加一个「赞赏页」:三个克制的决定,和两个静默的坑
AI 摘要
正在生成摘要…

起因#

在别人的博客上看到一张赞赏页(liushen.fun),做得很干净:上面两个收款码并排,下面一份鸣谢名单,整页没有一句”求打赏”的话术,就是安安静静放个入口。

本站一直没有这个页面。倒不是排斥,是觉得很多赞赏页做得让人不太舒服——金额明晃晃列一排、名字后面挂着「¥50」,读着像排行榜。所以我给自己定了三条:

  1. 金额一律不公开,名单只记昵称和留言;
  2. 数据手动维护,不接后台、不搞数据库,改一个文件就够;
  3. 收款码先占位,图后面再补,但页面现在就得能正常打开。

这三条看着是需求,其实每一条都对应一个实现上的选择。这篇文章就是把这几个选择、以及路上踩的两个坑记下来。

一、先拆原站:代码 0% 可搬,思路 100% 可复用#

第一件事是看对方怎么写的。结论很干脆:搬不了。

维度原站本站
主题Hexo + Pug 模板Fuwari(Astro)
模板语言Pug(缩进即语法)Astro 组件(JSX-like)
布局外壳主题自带 page 模板MainGridLayout.astro
样式方案独立 CSSTailwind + 主题 CSS 变量

Pug 和 Astro 的语法差异大到没法逐行翻译,硬套只会写出一堆不三不四的东西。但拆完之后有个收获:它真正值钱的不是布局,而是那几条克制——不写金额、只留留言、名单不排序。这些是设计决定,跟用什么主题无关。

所以接下来的工作不是”复刻”,而是”用 Astro 重新表达同一套决定”。想清楚这点之后,后面就顺了。

二、三个要求,各自落在哪#

1. 金额不公开 —— 让它根本不存在,而不是藏起来#

这是全篇我最想强调的一点。

「不公开金额」有三种常见做法,安全性差着量级:

做法实现风险
前端不渲染数据里有金额,模板里不显示改版、导出数据、看源码时随时漏出
CSS 遮起来金额渲染了,用 filter: blur 蒙住右键检查元素即破
结构里就没有数据类型压根没这个字段想漏也没得漏

我选第三种。数据结构长这样:

export interface Sponsor {
/** 昵称(必填,公开显示) */
name: string;
/** 头像地址;留空则显示昵称首字符 */
avatar?: string;
/** 留言 / 一句话简介 */
message?: string;
/** 支持日期,格式 YYYY-MM-DD;留空则不显示 */
date?: string;
/** 对方的主页或站点,填了会在卡片右上角显示外链按钮 */
url?: string;
}

没有 amount。这不是”暂时没用到”,而是刻意不给它留位置——将来就算我自己想加,也得先改类型定义、再改模板,多一道心理摩擦。约束做进结构里,比写进注释里可靠。

顺带一个细节:名单头部那个 共 N 位 的计数,也是唯一出现的数字。整页没有第二处金额相关的量。

2. 数据手动改 —— 收成一个文件,且不用手动排位置#

所有内容集中在 src/sponsor_data.ts,文件顶部写满了注释,包括图片该放哪、名单怎么写、不想署名怎么办。

真正省事的是排序。人的直觉是”往下追加最新的一条”,但页面上要的是”最新的在最上面”。如果每次加人都得手动挪位置,早晚会忘。所以把排序交给页面:

const sortedSponsors = [...sponsors]
.map((item, index) => ({ item, index }))
.sort((a, b) => {
const da = a.item.date ?? "";
const db = b.item.date ?? "";
if (da && db) return db.localeCompare(da); // 都有日期 → 倒序
if (da) return -1; // 只有 a 有 → a 在前
if (db) return 1; // 只有 b 有 → b 在前
return b.index - a.index; // 都没日期 → 按书写顺序,后写的在前
})
.map(({ item }) => item);

规则就三条:有日期的按日期倒序、没日期的排在后面、日期空缺时按写入顺序倒推。于是维护动作退化成”打开文件、往数组末尾粘一段、保存”,不需要读任何其他代码。

3. 收款码占位 —— 靠 onerror 回退,而不是靠我记得补图#

第三个要求最实际:正文和结构现在就能上线,二维码图我手上还没有。

如果直接 <img src="/sponsor/wechat.png">,图不存在时浏览器会渲染一个破图图标加 alt 文字,很难看。常见的笨办法是先放一张占位图,补真图时再替换——但那样就多了一步”记得改代码”。

我让缺图这件事自己暴露:

<img
src={way.qr}
alt={`${way.name}收款码`}
width="180"
height="180"
loading="lazy"
onerror="this.closest('.sponsor-qr-frame').classList.add('is-empty')"
/>

加载失败时给外层容器加一个 is-empty 类,CSS 里由它切状态:

.sponsor-qr-frame.is-empty img { display: none; }
.sponsor-qr-frame.is-empty .sponsor-qr-placeholder { display: flex; }

.sponsor-qr-placeholder 是一块虚线框 + 二维码图标 + “待补充”,平时 display: none 待命。

这么写的好处是:补图这个动作不需要改任何代码。把 wechat.png 丢进 public/sponsor/,刷新即生效。同款的还有名单头像——onerror="this.remove()" 直接摘掉失败的 <img>,露出底下用昵称首字符做的字母头像,永远不会看到破图。

三、入口:插进导航栏#

页面做好了得能进去。本站导航在 src/config.ts 的 navBarConfig 里,加一项即可:

{
name: "赞赏",
url: "/sponsor/",
external: false,
},

位置我放在「友链」和「开往」之间——同类站内页聚在一起,不是随手丢末尾。现在是:

首页 / 归档 / Now / 关于 / 友链 / 赞赏 / 开往 / 其他

注意 url 不写 base path(本站部署在子路径时由主题自动拼),external: false 表示站内链接、不显示外链图标。

四、两个坑,都是”不报错”的那种#

真正花时间的不是写页面,是这两处。

坑 1:编辑时缩进模糊匹配,把相邻项吃掉了#

加导航项时我用了编辑器做字符串替换,结果回读发现「开往」那一项被截断并重复了——本来一项变成了两项半。

原因是我给的匹配文本和文件里的真实缩进不完全一致(这文件用的是 Tab,我按习惯写成了空格)。编辑器做了容错匹配,把边界算歪了。这件事的教训不是”这个编辑器不行”,而是:

改配置文件后一定要回读校验,不能信”已保存”。

我的做法是改完立刻用脚本把改动区域的每一行按 repr() 打出来,逐行核对缩进字符:

lines = open(path, 'rb').read().decode('utf-8').split('\n')
for n, l in enumerate(lines[43:95], 44):
print(n, repr(l)) # repr 会把 Tab 打成 \t,一眼看出真假

在那之后,凡是涉及此类改动,我都走”写脚本改 + 回读核对”,而不是依赖编辑器的模糊匹配。批量替换一律用 Python 按字节处理,因为一旦涉及行尾(\r\n)和编码,文本模式的读写会静默做转换,污染整个文件。

坑 2:Tailwind 静默丢弃了那个类#

坑 1 是手滑,坑 2 是真不知道。

本站友链页有两个「申请友链」「申请标准」的小圆底图标,写法是:

<div class="w-10 h-10 rounded-full flex items-center justify-center bg-[var(--primary)]/10">

看起来完全合理——10% 透明度的主题色。但它一直是失效的,圆底其实没有颜色,只是不太明显所以没人发现。

原因:Tailwind v3 没法给 var() 任意值注入 alpha。bg-[var(--primary)] 是合法的,末尾加 /10 之后整个类会被直接丢弃——不报错、不警告,类名还老老实实挂在 DOM 上,所以只能靠肉眼”感觉这里应该有圈底色”来发现。

我用 Tailwind CLI 单独跑了一遍验证,不是猜的:

Terminal window
# in.css 里只有一行 @tailwind utilities;
# 测试用的 HTML 里放 bg-[var(--primary)]/10 和对照的 bg-white/10
node node_modules/tailwindcss/lib/cli.js -i in.css -o out.css --content test.html

结果:产物里 bg-white/10 正常生成,bg-[var(--primary)]/10 一行都没有。

修的办法很简单,用 color-mix() 手写:

.icon-badge {
@apply flex items-center justify-center shrink-0 w-10 h-10 rounded-full transition;
color: var(--primary);
background: color-mix(in oklab, var(--primary), transparent 90%);
}

顺手做了一件事:这既然是「主题色圆底 + 图标」这种通用视觉原子,就别让它在每个页面里重复一遍。提炼成全局类 .icon-badge 之后,赞赏页和友链页共用同一份定义,以后再出现同类用法直接套类名就行。

color-mix() 还有个额外好处:深色模式不用单独写覆盖。因为 --primary 本身在两个主题下取值不同,混出来的底色自动跟着变。

五、没起 dev server,怎么看效果#

本机内存比较紧,我不太愿意为看一眼样式就把 dev server 拉起来。所以这次的做法是手工拼一份静态预览:

  1. 用 stylus 编译主题真实的变量文件,拿到 --primary、--card-bg、--line-color 这些真值;
  2. 用 Tailwind CLI 对页面源码做一次按需编译,产出它真正用到的那批工具类;
  3. 把两者内联进一个自包含 HTML,附一份和源码一致的标记。

产出是一个能直接双击打开的文件,还能切浅色/深色,观感和线上基本一致。代价是这份预览不会自动跟随源码更新,改完要重跑一次生成脚本——对”看效果”这个用途足够了。

这一轮验证用了三样东西,都不起服务:

手段查什么
@astrojs/compiler 直接编译.astro 语法是否通过
Tailwind CLI 输出产物某个类到底有没有生成 CSS
Python 字节回读改动是否真的落盘、行尾是否被翻转

小结#

这一页的代码量不大——一个页面文件、一个数据文件、导航里加一项、全局 CSS 加一个类。真正值得记的是那几个刻意的选择:

  • 「不公开金额」不是把金额藏起来,而是让它在数据结构里不存在;
  • 「手动维护」不是把编辑成本留给人,而是让人只需要”往末尾粘一段”;
  • 「图片占位」不是先垫一张假图,而是让缺图自己触发一个状态。

以及那两个不报错的坑——模糊匹配会吃掉相邻内容,Tailwind 会静默丢弃不合法的类。它们的共同点是:出错时什么都不说,代码看着还是对的。对付这类问题只有一个办法,就是改完必须回读、必须拿产物验证,不能靠”看起来没问题”。

赞赏页已经上线,入口在导航栏「赞赏」。金额不公开这条会一直保持——那是我和每一位朋友之间的小默契。

AI 参与程度
润色
完全
不使用
给博客加一个「赞赏页」:三个克制的决定,和两个静默的坑
https://blog.142588.xyz/posts/fuwari-sponsor-page/
作者
Watch Your Back
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0

评论

加载中…