@ basenative/og-image
运行时不可知的OG /社会分享PNG渲染器- SVG卡场景栅格化
resvg-wasm,与CDN + kv缓存字体。运行在Cloudflare Workers, Node, Deno和Bun上。
部分 BaseNative 生态系统——基于信号的web运行时,基于原生HTML。
在请求时间渲染1200x630 png og:image, twitter:image,以及任意共享卡端点。
如果您是从0.2.x升级,请先阅读本文
renderPng -顿悟的入口点- 已移动 到一个单独的出口:
-import { renderPng } from "@basenative/og-image";
+import { renderPng } from "@basenative/og-image/satori";
和 satori 现在是一个 可选的对等依赖: 如果你自己安装
使用那条路径。在Cloudflare Workers上,你不能;使用 renderCard/renderSvg
而是从主条目,这就是这个版本存在的目的。
为什么
此软件包的每个版本,包括0.2.2 不能执行
Cloudflare所有工作人员,尽管是为他们写的。 satori 导入
harfbuzzjs (它的文本塑造器),以及harfbuzz的刻字加载器
按顺序问两个环境问题,工人回答错误。已复制
反对真正的工人 (wrangler dev, compatibility_date 2026-04-23,
nodejs_compat):
TypeError: Cannot read properties of undefined (reading 'href')- - - 加载程序将WorkerGlobalScope,结束“浏览器工作器”,并读取self.location.href。workerd定义WorkerGlobalScope和self, 定义了 否location.- 垫片
location在它之前,你会得到ReferenceError: __dirname is not defined- - - - - -nodejs_compat给工人一个process.versions.node,所以 loader接着结束“Node”,然后开始寻找hb.wasm在文件系统上 那是不存在的。牧马人不捆绑该文件,其余的 分支从缓冲区编译WASM,嵌入器完全拒绝 (Wasm code generation disallowed by embedder——也测量过)。
固定 (2) 需要 harfbuzzjs 接受铭文 Module.instantiateWasm
钩子是静态导入的 .wasm 可以上交。 hb.js 荣誉的钩子;
harfbuzzjs输入呼叫 hb() 没有参数,所以没有下游包可以
供应。一个牧人 [alias] 不能站在两者之一中-别名 harfbuzzjs
重写它的子路径,所以替换不能再解析真实的
hb.js/hb.wasm.
所以修复是不需要一个文本整形器。 @resvg/resvg-wasm 它自己
整形,它是一个单一的静态 .wasm 工人们接受。卡现在是一个
SVG字符串,并且布局引擎是 text.js -算术超过测量的字形
进步,无处不在。
安装
npm install @basenative/og-image
# only if you use the satori entry point:
npm install satori
快速启动
// src/worker.js — Cloudflare Worker
import { renderCard, pngHeaders } from "@basenative/og-image";
export default {
async fetch(request, env) {
const png = await renderCard(
{
title: "Northside Electrical",
subtitle: "Electrical — Manchester, NH",
badge: "Licensed electrician",
brand: "northside.example.com",
},
env,
);
return new Response(png, { headers: pngHeaders() });
},
};
测量于 wrangler dev: ~730ms的冷隔离(两个字体提取加上WASM)
加热26-45ms。
卡片
brandCard(opts) → string (SVG)
通用实体卡:一个名称、一个限定行、一个可选的徽章和一个 品牌线钉在底部。
import { brandCard, renderSvg } from "@basenative/og-image";
const svg = brandCard({
title: "Northside Electrical",
titleFallback: "northside", // used if `title` has no letter or digit
subtitle: "Electrical — Manchester, NH",
badge: "Licensed electrician",
brand: "northside.example.com",
theme: { accent: "#4EAF7C" },
width: 1200,
height: 630,
});
const png = await renderSvg(svg, env);
没有输入可以破坏布局。 每次运行都符合宽度预算,并且 整个堆栈安装到顶部填充和品牌线之间的高度。 一个空标题,一个200字符的标题,一个没有空格的标题,RTL脚本, 标记、控制字符和单独的代理都由测试覆盖,并且 所有渲染到有效的1200x630 PNG。
值得了解的文本处理:
- 收缩贴合,然后包裹,最多三行,然后是省略号。
- 柔软的优惠 后
-,/,_和.,所以一个呆滞的名字在greenleaf-/landscaping而不是中间词。 - Grapheme-安全截断 -永远不会发出一个单独的代理,这将使 文档无效的XML。
- RTL 集合
direction="rtl"当内容主要是RTL脚本时。 - XML-非法字符被删除,不转义 -杂散控制 字符在数据库列中,否则会将一个坏行变成500 公共页面。
字体覆盖是一个真正的限制
光栅化器只绘制给定的字体缓冲区中存在的字形,和
默认是拉丁Inter。一个CJK, Devanagari或阿拉伯语标题措施和
正确布局,然后呈现为 .notdef 盒子。 brandCard 限制了
损害-它总是吸引 brand 行,所以牌仍然标识它的
主题-但如果你提供这些脚本,提供一个覆盖面:
await renderCard(card, env, {
fonts: { urls: { 600: "https://…/NotoSansArabic-SemiBold.ttf" } },
});
titleFallback 覆盖完全没有单词字符的名称的较窄大小写
(仅限表情符号的业务名称): 相反,将绘制回退。
OG_SIZE
{ width: 1200, height: 630 } -高于Facebook的600x315最小值,正是
1.91:1的比例它的文件,和里面的2:1 .. 1:1窗口X需要为
summary_large_image.
字体
两个渲染路径需要两种格式,出错是无声的:
| 路径 | 格式 | 来源 | 为什么 |
|---|---|---|---|
renderSvg / renderCard | ttf | @expo-google-fonts/* (上游Google TTFs在不可变的npm版本) | resvg的 fontdb 只解析原始数据。给它一个 .woff 和它加载 零面不画任何东西 -一个正确大小的卡与文本丢失。 |
@basenative/og-image/satori | woff | @fontsource/* | satori解压WOFF本身;文件要小5倍。 |
import { defineFonts } from "@basenative/og-image";
defineFonts({
family: "Inter",
weights: [600, 800],
format: "ttf",
cdnVersion: "0.2.3",
cacheBinding: "OG_CACHE", // KV binding name; `null` = deliberately none
cacheKeyPrefix: "font:",
urls: { 600: "https://…/Custom-SemiBold.ttf" }, // per-weight override
buffers: { 600: bytes }, // skip the network entirely
});
缓存键包括格式和CDN版本,因此由satori缓存的WOFF 路径永远不能服务于光栅化路径。
无KV缓存
字体是用 cf: { cacheTtl: 86400, cacheEverything: true },所以
Cloudflare自己的边缘缓存即使在没有KV的情况下也能吸收冷隔离失误
名称空间。通过 cacheBinding: null 说这是故意和沉默
警告。KV:
wrangler kv namespace create OG_CACHE
[[kv_namespaces]]
binding = "OG_CACHE"
id = "<id>"
提供WASM模块
包解析resvg .wasm 本身:静态导入
牧马人/esbuild (提前编译-工人拒绝编译
从缓冲区中读取WASM),以及在Node下读取的文件系统。如果两者都不适合你
运行时或打包器,把它交出来:
import wasm from "@resvg/resvg-wasm/index_bg.wasm"; // your bundler
import { provideResvgWasm } from "@basenative/og-image";
provideResvgWasm(wasm); // WebAssembly.Module, bytes, a Response, or a promise
在第一次渲染之前,在模块作用域中调用它。你所提供的战胜了 内置的分辨率,然后从来没有咨询。
运行时检测
detectRuntime() 被导出,因为它的检查顺序是整个
此版本的主题:
import { detectRuntime, isWorkerd, hasFilesystem } from "@basenative/og-image";
detectRuntime(); // "workerd" | "node" | "deno" | "bun" | "browser" | "unknown"
- 在Node之前检查workerd,因为
nodejs_compat使it报告process.versions.node. - 在浏览器之前检查workerd,因为它定义了
WorkerGlobalScope和self。将它与真正的浏览器工作者区分开来的信号是 浏览器工作人员 有 一个location.
缓存图像本身
pngHeaders() 默认情况下,将响应标记为一年不可变,因为
预期的寻址方案是内容寻址的:放入卡自己的哈希值
因此,重命名主题会更改URL,并且不会留下爬虫
拿着一张陈旧的卡片。如果您的URL不是内容寻址的,请传递
{ immutable: false }.
API
| 出口 | 目的 |
|---|---|
renderCard(card, env?, opts?) | 创建一个品牌卡片并栅格化。 |
renderSvg(svg, env?, opts?) | 栅格化任何SVG字符串。 |
brandCard(opts) | 在不渲染的情况下构建卡片场景。 |
pngHeaders(opts?) | Content-Type + Cache-Control. |
provideResvgWasm(source) | 显式地提供WASM模块。 |
defineFonts, loadFontBuffers, loadFonts, fontUrl, fontCacheKey | 字体。 |
detectRuntime, isWorkerd, hasFilesystem, canCompileWasmFromBytes | 环境。 |
escapeXml, num, rect, textLine, textBlock, headline, clippedLine, svgDoc | SVG原语,用于自定义场景。 |
graphemes, estimateWidth, isRtl, hasWordCharacter, truncateGraphemes, truncateToWidth, wrapToLines, fitText | 文本布局。 |
OgImageError | 用马槽扔 code. |
box, text, tile, tileGrid, parseGrid, theme, defaultTheme, el, defaultPreset, articlePreset, scoreCardPreset, presets | satori场景DSL-仅限数据; 通过 /satori 条目。 |
@basenative/og-image/satori 另外出口 renderPng(scene, env?, opts?).
主题
brandCard({
title: "Hello",
theme: { bg: "#0a0a0a", fg: "#fafafa", accent: "#7c3aed", muted: "#71717a" },
});
令牌: bg, fg, accent, muted, tile, letter, green, yellow,
absent, empty.
错误
OgImageError 带着马厩 code:
| 代码 | 意义 |
|---|---|
satori-unsupported-on-this-runtime | 你打电话给工人的satori入口点。 |
wasm-source-unavailable | 既不是导入映射也不是 provideResvgWasm 产生了一个模块。 |
wasm-source-invalid | provideResvgWasm 被交给的东西不是一个模块,字节或 Response. |
font-fetch-failed | CDN拒绝了,或者没有TTF知道这个家族/重量。 |
no-fonts | 解析的权重列表是空的; 光栅化器什么也不画。 |
许可证
apache - 2.0