圣堂之魂
博客随笔

给博客加上分享海报

从 Fumadocs 原生分享到借鉴 Mizuki 主题的海报生成方案,记录一次组件重构的过程


起因

博客搭好之后,一直琢磨着给博客加一个分享功能。但我对一件事特别在意:几乎所有 App 的分享链接,后面都拖着一串 ?from=xxx&utm_source=yyy。你说它是统计来源吧,我理解,但作为一个普通读者,我只想复制一个干干净净的 URL,而不是一长串占位乱码。

我的博客不需要那些花里胡哨的点击计数,我就想要一个清爽的短链接。

可直接用原始链接呢?又太长,尤其文章路径带中文或日期,发出去像裹脚布。

这时候我把目光投到了我自己的短链服务上:Short_NURL,一个基于 OpenResty + PHP 开发的短链压缩服务。

Short_NURL 之前已经单独写过一系列文章详细介绍,感兴趣的话可以去翻翻。

跳转极快,并发扛得住。它本来就是干“长链接压缩”这活儿的,那正好拿来用。

而且,这不也是个绝佳的机会么?让我的开源项目真正跑在自己的博客里,比什么 demo 都有说服力。

于是二话不说,把分享按钮和 /api/short-url 一接,点击就生成类似 https://n.uoca.top/xxxx 的短链,干净利落。


但……只有短链,还是太干巴了

短链解决了长度问题,可分享出去就一个链接,读者看一眼就划走了,没欲望保存,也没欲望转发。

总觉得缺了点“感染力”。

我开始琢磨:能不能在分享的时候,附带一张好看的图?就像朋友圈那些知识卡片一样,有标题、有封面、有作者,扫个码就能看全文。

那样才叫分享嘛。


寻找灵感

既然决定了做一张海报,就得想清楚海报该长什么样。

顺着这个思路,我开始翻其他博客主题的做法。大多数还是链接+二维码的老套路,顶多放张封面图。直到翻到 Mizuki 主题的分享海报,才觉得“对了”。

它不是简单地把链接和二维码堆在一起,而是用 Canvas 绘制了一张完整的图片:顶部是封面图,左下角叠日期徽章,中间是加粗的标题,底部有头像、作者名和二维码。整体布局干净,配色舒服,一看就是花了心思的。

短链解决实用性,海报解决传播性。

这不就解决问题了吗。

所以,我决定把 Mizuki 的那套绘制逻辑借鉴过来,给我的博客也加上海报生成。

Mizuki 的设计让人眼前一亮。它不是简单地展示一个二维码,而是用 Canvas 绘制了一张完整的图片:顶部是封面区域,左下角叠加了一个日期徽章,中间是加粗的标题文字,下方有作者头像和二维码,整体布局干净利落,配色也很舒服。

这种"生成一张真正的图片"的思路,正是我想要的。


借鉴与实现

既然看到 Mizuki 的解法正合我意,本着不重复造轮子的精神,我直接把它仓库里的海报生成代码拉下来看看。

看一眼许可证:Apache 2.0 & MIT 双许可,基本上等于“随便用”。

这下心里有底了。再一看技术栈,Mizuki 是 SvelteKit,我这是 Next.js,但核心的 Canvas 绘制逻辑跟框架无关,而且大家都是 Node.js 生态,迁移起来毫无难度。

所以我的做法很简单:直接把核心绘制函数复制过来,只把组件的生命周期从 Svelte 的 onMount 改成 React 的 useEffect,其他基本原样照搬。

核心的几个工具函数包括:

Prop

Type

图片加载我也顺手改了一下。Mizuki 原版有个通过 images.weserv.nl 代理的 fallback,但国内访问不稳定,经常超时,我干脆去掉,加载失败直接显示占位符,别让一张图拖慢整个生成。

Mizuki 主题采用 Apache 2.0 & MIT 双许可,在此感谢 Mizuki 的开发者。

深色模式支持

Mizuki 的海报默认只有亮色模式。但我的博客是支持深色主题的,如果海报不能跟着切换,体验会很割裂:暗色页面里突然弹出一张亮白色的图片,视觉上非常突兀。

于是我决定在 Mizuki 的配色方案基础上,增加一套完整的深色模式调色板。

做法不复杂:在生成海报之前,先检测当前页面是否处于深色模式(通过检查 document.documentElement 上的 dark class),然后根据检测结果选择对应的配色方案。

除了自动检测,海报底部还提供了一个手动切换按钮。用户可以覆盖系统设置,选择生成亮色或暗色的海报。


最终效果

海报的布局从上到下分为四个区域:

  1. 顶部区域 : 封面图或主题色背景,左下角叠加日期徽章
  2. 标题区域 : 加粗显示文章标题,支持自动换行
  3. 描述区域 : 左侧带竖线装饰的摘要文字
  4. 底部区域 : 头像、作者名、"扫码查阅文章"提示、二维码

底部操作栏提供三个按钮:

  1. 复制链接 : 一键复制文章短链接
  2. 保存海报 : 下载生成的 JPEG 图片
  3. 亮暗切换 : 一个小图标按钮,点击后切换海报的配色方案

整个交互流程是:点击分享按钮 → 短链 API 返回短链接 → Canvas 绘制海报 → 弹窗展示。生成过程在客户端完成,不依赖服务端渲染。


遇到的坑

过程中踩了几个小坑,记录一下:

Canvas ref 问题 : 最初把 <canvas> 元素渲染在 JSX 中,通过 useRef 获取引用。但 React 的渲染时序有时会导致 ref 在 useEffect 中为 null。后来改成用 document.createElement('canvas') 在内存中创建,完全绕开了 DOM 引用的问题。

图片加载超时 : 二维码 API 和头像都可能加载失败。最初没有设置超时,导致整个生成流程卡住。加上 Promise.race 和超时机制后解决了。

暗色模式切换闪烁 : 每次切换亮暗模式,generating 状态会被重置为 true,导致旧海报消失、显示“生成中”、新海报出现,视觉上很跳。修复方案是:只有首次生成时才显示 loading 状态,后续切换直接静默替换。


致谢

海报组件的核心绘制逻辑借鉴了 Mizuki 主题的实现,在此对主题开发者表示衷心的感谢。

MIT License
MIT License

Copyright (c) 2023 saicaca

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache 2.0 License
Copyright 2025 Matsuzaka Yuki

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

本页目录