搜索 K
主题
@vaultkit/vitepress-plugin-og-image 在 VitePress buildEnd 钩子里为每个页面生成一张 Open Graph 分享图(1200 × 630 PNG)。管线是 satori(元素树 → SVG,纯计算)+ resvg(SVG → PNG),全程无浏览器——CI 里不需要 Puppeteer / Playwright,客户端零成本。
针对中文站点它做了一件关键的事:完整 CJK 字体动辄数 MB,不能整体喂给 satori;本包先收集全部待渲染文本的码点,再按 Fontsource 的 unicode-range 元数据只加载覆盖到的字体子集——典型博客站最终只载入个位数的 30-45KB 小文件。这是纯构建期包,本文的产物说明与配置示例全部用代码围栏呈现。
链接被分享到社交平台 / 聊天软件时,抓取器读的是页面的 og:image meta。没有专图的页面要么显示空白卡片,要么全站共用一张兜底图——每篇文章的标题、标签这些现成信息全浪费了。
逐页生成 OG 图的传统方案是起一个 headless 浏览器截图,代价是 CI 里装 Chromium(数百 MB)、冷启动秒级、并发内存爆炸。satori 换了条路:不渲染 HTML,而是接收一棵受限的元素树(Flexbox 子集样式),纯计算排版直接吐 SVG;resvg(Rust 实现的 SVG 渲染器,走 napi 绑定)再把 SVG 光栅化成 PNG。整条管线是普通的 Node 函数调用,每页约 50-150ms。
pnpm add -D @vaultkit/vitepress-plugin-og-image依赖三件:satori、@resvg/resvg-js、@fontsource/noto-sans-sc(字体子集来源)。
使用 @vaultkit/vitepress-theme-vault 主题则开箱即带:features.ogImage 缺省为 true,主题按 og/<slug>.png 契约为每篇已发布笔记生成专图,并在 transformPageData 里逐页注入 og:image meta。关闭方式:
export default defineConfigWithTheme({
extends: createVaultTheme({
site: { /* ... */ },
features: { ogImage: false },
}),
})import { generateOgImages } from '@vaultkit/vitepress-plugin-og-image'
import { defineConfig } from 'vitepress'
export default defineConfig({
async buildEnd(config) {
await generateOgImages(config, {
siteTitle: '我的数字花园',
pages: posts.map(post => ({
file: `og/${post.slug}.png`, // 相对 outDir 的产物路径
title: post.title,
subtitle: post.date,
tags: post.tags,
})),
})
},
})注意本包只负责生成图片文件;逐页的 og:image meta 标签由宿主注入(如 transformPageData),两侧路径遵守同一 file 契约。构建结束打印 [og-image] 已生成 N 张分享图(M 个字体子集,Xms)。
| 导出 | 形态 | 说明 |
|---|---|---|
generateOgImages | 异步函数 | 生成入口,(config, options) => Promise<void>;pages 为空数组时直接返回 |
defaultTemplate | 函数 | 缺省模板 (page, ctx) => OgElement,自定义模板可参考或复用 |
OgImageOptions | 类型 | 选项接口 |
OgImagePage | 类型 | 单页数据 |
OgFont | 类型 | satori 字体项 |
OgElement | 类型 | satori 无 JSX 形态的元素树节点 |
OgTemplateContext | 类型 | 模板上下文 { siteTitle, width, height } |
OgSiteConfig | 类型 | SiteConfig 的结构化子集(只消费 outDir) |
stringOgImagePage[]number,默认 1200 / 630summary_large_image、Telegram、微信都按此裁切最友好,没有特殊需求不要改。OgFont[],默认从 @fontsource/noto-sans-sc 按码点需求加载 400/700 两字重的最小子集组合OgFont 逐字段:| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 字体族名,与模板样式里的 fontFamily 对应 |
data | ArrayBuffer | Buffer | 字体二进制(TTF / OTF / WOFF,不支持 WOFF2) |
weight | number | 字重(可选) |
style | 'normal' | 'italic' | 字形(可选) |
自定义品牌字体示例:
import { readFile } from 'node:fs/promises'
await generateOgImages(config, {
siteTitle: '数字花园',
pages,
fonts: [
{ name: 'LXGW WenKai', data: await readFile('fonts/LXGWWenKai-Regular.ttf'), weight: 400 },
{ name: 'LXGW WenKai', data: await readFile('fonts/LXGWWenKai-Bold.ttf'), weight: 700 },
],
})(page: OgImagePage, ctx: OgTemplateContext) => OgElement,默认 defaultTemplateOgElement 是 React 元素结构的纯对象子集({ type, props: { style, children } }),无需 JSX 与 React 依赖:import type { OgElement, OgImagePage, OgTemplateContext } from '@vaultkit/vitepress-plugin-og-image'
function myTemplate(page: OgImagePage, ctx: OgTemplateContext): OgElement {
return {
type: 'div',
props: {
style: { width: ctx.width, height: ctx.height, display: 'flex', backgroundColor: '#fff', fontFamily: 'Noto Sans SC' },
children: page.title,
},
}
}satori 只支持 Flexbox 布局子集:每个多子节点的 div 都要显式 display: 'flex',不支持 grid / float。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | string | 是 | 相对 outDir 的产物路径(如 'og/minimal-theme.png'),中间目录自动创建 |
title | string | 是 | 页面标题,缺省模板的视觉主体 |
subtitle | string | 否 | 副行(日期 / 摘要等),缺省模板渲染在标题下方、最多 2 行 |
tags | string[] | 否 | 标签,缺省模板取前 4 个渲染为胶囊行 |
缺省模板生成的 PNG 构图(自上而下):
#3451b2 → #8a63d2,即 VitePress 品牌蓝到紫)135deg,#1b1b1f → #26262c → #2d3348),颜色对齐 VitePress 暗色变量的静态值,分享卡片与站点暗色态视觉同源lineClamp: 3);其下可选 26px 灰色副行(最多 2 行);再往下是标签胶囊行——圆角 999 全圆胶囊、半透明靛蓝底、浅紫蓝文字,最多 4 枚space-between 上下撑开,四周留 64/72px 内边距一句话概括:深色渐变卡片上的「大标题 + 元信息 + 站点署名」,不放插图不放头像,信息密度与可读性优先。
const svg = await satori(template(page, ctx), { width, height, fonts })
const png = new Resvg(svg, { fitTo: { mode: 'width', value: width } }).render().asPng()两步都是纯函数式调用:satori 做文字排版(用字体度量精确计算断行、对齐、截断),resvg 做光栅化。没有浏览器进程、没有 GPU、没有沙箱开销,CI 镜像不需要任何额外系统依赖——@resvg/resvg-js 通过 napi 预编译二进制随 npm 安装即可用。
Fontsource 把 Noto Sans SC 按 unicode-range 切成约 100 个子集文件(每个 30-45KB WOFF),并在包内附带 unicode.json 元数据(子集名 → 码点区间)。插件的装载流程:
siteTitle 与所有 pages 的 title / subtitle / tags 拼成一个大字符串,逐字符收集码点集合unicode.json 里每个子集的区间表达(支持 U+4e00-9fff 区间与 U+4??? 通配两种形态)典型博客站的标题文本只命中个位数子集,字体装载从「数 MB 整字体」降到「几百 KB 按需小文件」,构建内存与耗时都可控。文本一个子集都没命中时直接抛错(提示检查 pages 文本),子集文件缺失(Fontsource 布局变化)时报错指向依赖版本排查。
产物不做跨构建缓存:每页 50-150ms、12 页量级全量小于 4 秒,缓存失效判定(模板变了吗?字体变了吗?)的复杂度远高于重算成本。
Q:自定义字体不生效 / satori 报字体解析错误? satori 支持 TTF / OTF / WOFF,不支持 WOFF2。Fontsource 包里通常同时带 woff 与 woff2,注意取 .woff;从 Google Fonts 下载则取 TTF。
Q:标题里的 emoji / 生僻字变成空白? 缺省字体只覆盖 Noto Sans SC 收录的简中/拉丁字形。需要 emoji 时传自定义 fonts 补充(如 Noto Emoji),或在标题里避免使用。
Q:报 [og-image] 待渲染文本未命中任何字体子集?pages 里的文本全是缺省字体覆盖外的字符(如纯 emoji 标题)。检查文本内容,或换用覆盖所需字符的自定义 fonts。
Q:图片生成了但分享卡片不显示? 本包只写 PNG 文件,og:image meta 要宿主自己注入且必须是绝对 URL(https://example.com/og/xxx.png)。用主题则两侧已按同一契约对齐。