给友链页加了悬停预览:12 张截图离线截好,主题文件一行没动

1612 字
8 分钟
给友链页加了悬停预览:12 张截图离线截好,主题文件一行没动
给友链页加了悬停预览:12 张截图离线截好,主题文件一行没动

今天翻 blog.amamo.top 的友链页,鼠标往卡片上划过的时候,卡片顶部长出了一条对方首页的截图。

效果大概是这样,不悬停是普通卡片:

未悬停时的友链卡片,头像加名字和描述
未悬停时的友链卡片,头像加名字和描述

划上去之后,卡片顶部展开一条首页截图:

友链卡片悬停时顶部展开对方首页截图
友链卡片悬停时顶部展开对方首页截图

不用点进去,扫一眼就知道对方站点长什么样。我那页友链现在还是一排光头像加名字,差距挺明显的。

他那边怎么做的#

顺手把他的实现扒出来看了。核心是两行 CSS:

.friend-card__cover {
height: 0;
opacity: 0;
transition: height 0.3s ease, opacity 0.35s ease;
}
.friend-card:hover .friend-card__cover {
height: 7rem;
opacity: 1;
}

截图默认高度 0、完全透明,悬停时展开到 7rem 并淡入。卡片本身被重写成了竖版:封面在上,头像加站点名、域名、描述在下。仓库在 qwc-ch/Firefly。

然后是图从哪来的。他的封面地址长这样:

https://tu.520781.xyz/file/youlian/blog.cuteleaf.cn.png

路径里带着域名、还统一放在 youlian 目录下,估计是他自己一张张截好、批量传上图床的。

我这边的约束#

我的博客是 fork 自 CuteLeaf/Firefly 的,上游一直在更新,所以给自己定了条规矩:src/pages、src/components、src/styles 这些主题源文件一律不动,要加东西就往 src/config、src/content、public 里塞新文件。

好处是上游发新版我直接 merge,冲突基本为零。代价是很多”顺手改一行”的事得绕路。

友链卡片正好在 src/pages/friends.astro 里渲染,撞红线上了。所以这个功能只能照着他的效果另找做法。

截图从哪来#

先把图的问题解决掉。摆在面前的有三个选项:

方案首次显示维护成本国内速度
第三方截图 API(thum.io、mshots 这类)5~15 秒零绕一圈国外
手动截图传图床快每加一条友链重做一次看图床脸色
本机离线截好存进仓库快跑一条命令同源,最快

第一个方案不用试:这类服务是”你请求它、它现去打开那个网站截图”,第一次必然等好几秒。加上国内访问那些服务本身就慢,卡片展览会变成幻灯片。

第三个方案的代价是仓库多 190 KB、多一个脚本要维护。我选了它。

离线截图脚本#

新增 scripts/generate-friends-covers.ts,跑一条命令把启用中的友链全截一遍:

Terminal window
npx tsx scripts/generate-friends-covers.ts

它干这几件事:

  1. 从 friendsConfig 里取启用中的友链;
  2. 调本机的 chrome-headless-shell 逐个打开首页截图;
  3. 用 sharp 转成 640 宽的 webp,存进 public/assets/images/friends/covers/;
  4. 生成 src/data/friends-covers.json 映射清单。

已经截过的会跳过,所以以后加了新友链再跑一次,只补新的那一张。

截图用的参数:

Terminal window
--headless --no-sandbox --disable-gpu --hide-scrollbars
--force-device-scale-factor=1
--window-size=1280,512
--virtual-time-budget=10000

--window-size 里的高度特意给得矮。卡片上那块封面是 7rem 高、宽度跟着卡片走,比例大概 2.5<1>,用 1280×512 截出来的正好是首页顶部(导航加首屏),比截个完整的 1280×800 再让 CSS 居中裁掉上下更合适。

Note

--virtual-time-budget 是让浏览器等网络空闲,它不推进页面里的 setTimeout。有站点靠定时器延迟渲染首屏的话,截出来可能是空白。我这 12 个站没遇到,但这个坑先记着。

12 个站跑完,一次成功,零失败。单张 7~45 KB,加起来 190 KB。

卡片是浏览器现场拼的#

图有了,剩下的问题是怎么塞进卡片。

不能改 friends.astro,那就换个思路:卡片结构留给主题,封面用一段独立脚本在浏览器里补上去。

原卡片是个横向 flex:左边 64×64 头像,右边名字、描述、标签。要让它顶部能展开一条通栏封面,把布局改成两列网格最省事:

.friends-grid .friend-card {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
column-gap: 0.75rem;
}
.friends-grid .friend-card > .ff-friend-cover {
grid-column: 1 / -1; /* 占满整行 */
}

这里占了个便宜:卡片里那两个装饰元素(悬停时的主题色底块、右上角的箭头图标)本来就是 position: absolute,在网格里不参与自动排布。所以真正进网格的只有封面、头像、内容三块,位置正好排成”封面一行、头像和内容一行”。

封面首屏是不加载的。脚本给 img 挂的是 data-src,只有卡片真的被悬停或聚焦过,才把地址写进 src:

["mouseenter", "focusin", "pointerdown", "touchstart"].forEach(function (name) {
card.addEventListener(name, reveal, { once: true, passive: true });
});

/friends/ 一页 12 张图,全铺上去就是 190 KB。现在第一次进去是 0 张,划过哪张加载哪张。

数据接口和注入点#

映射表走了一个构建期生成的静态 JSON:

src/pages/api/friends-covers.json.ts

加了 export const prerender = true,构建时把 friendsConfig 和截图清单合成一份文件,访客拿到的就是 dist/api/friends-covers.json。没有服务端,也没有运行时开销。

脚本怎么进到 /friends/ 页面,这一步卡了一会儿。

最自然的做法是写进 friends.mdx,但那个页面的内容区在 #swup-container 里 —— swup 无刷新切页时,新插进 DOM 的 <script> 标签浏览器不会执行。从首页点进友链页,脚本就等于没加载。

所以最后落在页脚注入点(src/config/FooterConfig.html)上。它在 swup 容器之外,整页加载时执行一次,然后挂住 swup 的切页事件:

document.addEventListener("astro:page-load", loadFriendCovers);
document.addEventListener("swup:page:view", loadFriendCovers);

页脚里只放十几行判断,真正的逻辑在 /assets/js/friends-covers.js,而且这个脚本只有 /friends/ 才会去拉,其他页面不背这个包。

性能账#

项目成本
/friends/ 首屏0 张封面图请求
悬停一张卡片1 张 webp,7~45 KB,之后走浏览器缓存
友链脚本本体4 KB,只在 /friends/ 加载
其他页面页脚多十几行判断,约 0.5 KB

一个知道但暂时没解决的问题#

悬停展开时卡片会从 94px 长到 206px,网格行高跟着变,所以同一行下面的卡片会被往下顶一下。

参考站也是这个行为(他的 CSS 就是 height: 0 → 7rem),我按他的做法来。想要完全不跳,得把封面改成绝对定位的浮层,那就不是他这个效果了,先放着。

以后加友链#

Terminal window
npx tsx scripts/generate-friends-covers.ts

截图脚本会跳过已截过的,只补新增那条,然后重写一遍清单。跑完构建带上线就完事。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
给友链页加了悬停预览:12 张截图离线截好,主题文件一行没动
https://www.9ll.uk/posts/blog/friendcoverpreview/
作者
HY
发布于
2026-10-04
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
给书签导航加悬停预览:复刻友链流程,多踩三个坑
博客书签导航那 64 个书签也加上了悬停预览:鼠标放上去展开一张该站点的首页截图。做法是把友链页那套原样搬过来,但书签和友链不是一个东西,中途多踩了三个坑——同域名多条会互相覆盖、Cloudflare 的 403 页截出来是张「看着正常」的白底黑字图、headless 的默认 UA 直接被拒。最后 56 条拿到了能看的封面。
2
给博客加了张更新日志页
博客羡慕 rainzt.cn 的博客日志页,把它的页面结构扒下来,用 Firefly 自己的组件体系重做了一遍。全程只新增三个文件,不碰主题源码,以后合并上游更新不受影响。顺便把折叠动画从 max-height 换成了 grid-template-rows。
3
把 Firefly 魔改了五十一版:一份完整的功能清单
博客博客是 fork 的 CuteLeaf/Firefly,从 V1.0 到 V1.49 一共 51 版改动。这篇不再讲故事,直接把改过的功能全部列出来:首页那套贴纸卡片、自己加出来的那批页面、友链与书签的悬停预览和自助申请、全站卡片描边与页脚统计、评论区的折叠与引用、右栏那几个自建组件、一长串性能优化,还有内容与主题分离这类基础设施。每条都附上它到底干什么、关键数字是多少。
4
给博客装了个朋友圈:把友链的更新聚成一条时间线
博客羡慕 rainzt.cn 的朋友圈,把它搬进自己的 Firefly:构建时抓取所有友链的 RSS 合流成时间线,快照兜底,GitHub Actions 每天自动刷新。纯静态,不用服务器,只新增文件不碰主题源码。后来又给时间线加了层来源配额,不让聚合站霸榜。
5
给博客加了个 Steam 游戏库:没后端,蹭朋友圈的定时任务喂数据
博客参考站用 Halo 插件在服务端拉 Steam Web API,我这静态站没后端,改成跟朋友圈同一套定时快照,顺带补了没 Key 时的降级路径。
随机文章随机推荐

评论区

Profile Image of the Author
HY
Hello, I'm HY.
公告
迷茫.... 为了访问速度,本站将会在11月上旬至中旬迁移到0dm.cn并完成备案
分类
标签
最新动态
站点统计
文章
74
分类
8
标签
192
总字数
161,979
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Vercel
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录