
HY
折腾进行时
Hello, I'm HY.








友链页那个悬停预览做完之后,我自己挺喜欢:鼠标划过去卡片顶部长出一条对方首页的截图,不用点进去就知道对面长什么样。
在书签导航页翻的时候发现这边就没这待遇 —— 一排图标加标题,干巴巴的。
于是就有了这篇。想法很简单:书签页和友链页结构几乎一样,都是「一个链接一张卡片」,把友链那套搬过来不就行了。
结果搬是搬过来了,坑比预期多。
友链那套实现上次已经写过一篇(给友链页加了悬停预览),这里只列一下文件对应关系:
| 友链 | 书签 |
|---|---|
scripts/generate-friends-covers.ts | scripts/generate-booknav-covers.ts |
public/assets/images/friends/covers/ | public/assets/images/booknav/covers/ |
src/data/friends-covers.json | src/data/booknav-covers.json |
src/pages/api/friends-covers.json.ts | src/pages/api/booknav-covers.json.ts |
public/assets/js/friends-covers.js | public/assets/js/booknav-covers.js |
public/assets/css/friends-covers.css | public/assets/css/booknav-covers.css |
数据源从 friendsConfig 换成 booknavConfig,匹配的字段从友链的 siteurl 换成书签的 item.url,其余逻辑一行没改。
截出来大概是这样,一张 640 宽的 webp:

到这里为止都很顺。下面三个坑全是「书签和友链不一样」的地方。
友链基本一站一个域名,文件名直接用域名扫就行:
1blog.cuteleaf.cn → blog-cuteleaf-cn.webp书签不是。我这边 github.com 底下挂了 7 个仓库 —— Firefly 上游、我自己的 fork、CC Switch、BewlyCat 之类的,全都在同一个域名下。
按域名取文件名,这 7 条会互相覆盖,最后只剩一张,而且还不确定剩的是哪张。
改法是把路径也编进文件名:
1github.com → github-com.webp2github.com/keleus/BewlyCat → github-com-keleus-bewlycat.webp3github.com/hy4962/Firefly → github-com-hy4962-firefly.webp万一连路径都一样,再补 -2、-3。清单里的 key 还是原始 URL,所以文件名怎么变都不影响匹配。

这个是最阴的。
第一轮跑完我随手翻产物,发现 deviantart 那张是 Cloudflare 的 403 页。
截出来是一张白底黑字、排版很规整的页面:
1403 ERROR2The request could not be satisfied.3Request blocked.问题在于 —— 它长得跟一张正常截图没有任何区别。文件大小正常(21 KB),宽高正常,打开是张像模像样的图。光看产物根本发现不了,得悬停展开才知道是个报错页。
所以必须在截图那一步就把它筛掉。
好在 chrome 的 --dump-dom 和 --screenshot 可以在同一次调用里同时用 —— 一个把渲染后的 HTML 打到 stdout,一个写图片文件,一次请求两样都拿到,不用多跑一遍:
1--screenshot=<png 路径> --dump-dom <url>拿到 HTML 之后,只看前 4000 个字符,命中这些特征就当失败:
1const BLOCKED_MARKERS = [2 /the request could not be satisfied/i,3 /\b403\s+error\b/i,4 /you have been blocked/i,5 /attention required/i,6 /just a moment\.\.\./i,7 /enable javascript and cookies to continue/i,8 /checking your browser before accessing/i,9 /verify you are human/i,10];只看开头是有意的:正文里偶尔出现「access denied」不算,标题和首屏出现才算。
另外还补了一道,用 sharp 算一下三个通道的标准差,整页一个颜色的空白图直接丢:
1const stats = await sharp(pngPath).stats();2if (stats.channels.every((channel) => channel.stdev < 3)) {3 throw new Error("截图几乎是纯色空白页,已丢弃");4}这一道抓的是另一种情况:站点还在渲染,图就先截下来了,出来是张纯白的。后来查失败清单,動漫花園、TouchGal 那几个就是栽在这。
chrome-headless-shell 的默认 UA 里带 HeadlessChrome 字样,Cloudflare 那批站点认得出来,直接回验证页。
换成正常的 Chrome UA 之后通过率明显上来:
1--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36前面都是截图的事,这一步是布局。
书签卡片原来是个横向 flex:左边 14×14 的图标,右边标题和描述。要让顶部能展开一条通栏封面,得把布局改成两列网格:
1.booknav-grid .booknav-card {2 display: grid;3 grid-template-columns: auto minmax(0, 1fr);4 column-gap: 0.75rem;5 row-gap: 0;6}7.booknav-grid .booknav-card > .ff-booknav-cover {8 grid-column: 1 / -1; /* 占满整行 */9}三个细节和友链不一样:
row-gap 要显式归零。 卡片上原本有 Tailwind 的 gap-3,在 flex 下只影响横向间距,改成 grid 之后它会同时变成行间距,封面和下面那行之间会凭空多出 0.75rem。所以得单独写 row-gap: 0。
负边距的数值跟着卡片 padding 走。 封面要顶到卡片边缘靠的是负边距,这个值必须等于卡片的 padding。友链那边是 p-2.5(0.625rem),书签这边是 p-3(0.75rem),照抄友链的会露一圈边。
网格要 align-items: start。 默认是 stretch,一张卡片展开会把同一行的邻居一起拉高。
「.booknav-card 是 flex」这件事本身是写在 Tailwind 类里的,我的样式是运行时注入的 <link>,未分层,压得过 @layer utilities,所以直接覆写就行,不用加 !important。
这部分和友链完全一样,顺手做了个小整理。
页脚(src/config/FooterConfig.html)里原本挂着一段友链专用的加载器,只在 /friends/ 才去拉真正的脚本。这次要再加一个书签的,与其复制一段,不如改成数组:
1var PREVIEW_SCRIPTS = [2 { path: /^\/friends\/?$/, id: "ff-friend-covers-js", src: "/assets/js/friends-covers.js" },3 { path: /^\/booknav\/?$/, id: "ff-booknav-covers-js", src: "/assets/js/booknav-covers.js" },4];按 pathname 正则匹配,命中的才插 <script>。以后哪页还要加同款预览,往数组里补一条就行。
映射表走 src/pages/api/booknav-covers.json.ts,加了 export const prerender = true,构建时生成一份静态 JSON,访客拿到的是文件不是接口。
封面图首屏一张都不加载 —— 脚本给 img 挂的是 data-src,卡片真被悬停或聚焦过才写进 src。
64 个书签,56 个拿到了能看的封面,加起来 812 KB。
失败的 8 个:
| 站点 | 原因 |
|---|---|
| DeviantArt | Cloudflare 403,被特征匹配拦下 |
| 末日動漫資源庫 | 同上 |
| 動漫花園 | 纯色空白页 |
| GYING 动漫筛选 / 发布页 | 纯色空白页 |
| TouchGal | 纯色空白页 |
| NekoGAL | 超时 |
| DeepSeek 控制台 | 超时(要登录) |
没有封面的书签卡片保持原样,悬停不展开,也不会留个破图。
全量跑一遍大概 9 分钟,主要是等每个站加载。已经截过的会跳过,所以加完新书签再跑一次,只补新的那几张,两三分钟的事。
1npx tsx scripts/generate-booknav-covers.ts想单独补某一条:
1npx tsx scripts/generate-booknav-covers.ts --only=touchgal全量重来加 --force,只想试水加 --limit=5。
--dump-dom 和 --screenshot 能一次调用同时拿 DOM 和图片,校验几乎零成本。row-gap 归零、负边距对齐卡片 padding。如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
部分内容可能已过时
分享你的想法,与大家交流讨论