| name | satori-og |
| description | Satori + @resvg/resvg-js OG image 產生最佳實踐指南。當需要產生 Open Graph 預覽圖、設定 OG/Twitter Card meta tags、或整合 Astro build-time image 產生時使用。 |
Satori + resvg OG Image Generation Guide
版本資訊
| 套件 | 版本 |
|---|
| satori | 0.26.0 |
| @resvg/resvg-js | 2.6.2 |
npm install satori @resvg/resvg-js
satori 0.26.0 新增:內建 JSX runtime,不再需要 React。
基本用法
方式一:Plain objects(無需 transpiler)
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import fs from 'node:fs';
const inter = fs.readFileSync('./public/fonts/Inter-Regular.ttf');
const interBold = fs.readFileSync('./public/fonts/Inter-Bold.ttf');
const FONTS = [
{ name: 'Inter', data: inter, weight: 400 as const, style: 'normal' as const },
{ name: 'Inter', data: interBold, weight: 700 as const, style: 'normal' as const },
];
const svg = await satori(
{
type: 'div',
props: {
style: { display: 'flex', color: 'white', fontSize: 48 },
children: 'Hello World',
},
},
{ width: 1200, height: 630, fonts: FONTS }
);
const resvg = new Resvg(svg, {
fitTo: { mode: 'width', value: 1200 },
font: { loadSystemFonts: false },
});
const png = resvg.render().asPng();
方式二:內建 JSX runtime(0.26.0+)
const svg = await satori(
<div style={{ display: 'flex', color: 'white' }}>Hello</div>,
{ width: 1200, height: 630, fonts: FONTS }
);
支援的 CSS 子集
Layout:display(flex/none)、position(relative/absolute)、所有 flexbox 屬性(flexDirection、gap 等)
Sizing:width/height/min/max(不支援 min-content/max-content/fit-content)
Typography:fontFamily、fontSize、fontWeight、fontStyle、textAlign、lineHeight、letterSpacing、textOverflow(clip/ellipsis)、textShadow、lineClamp
Visual:backgroundColor、backgroundImage(gradient/url)、borderWidth/Style/Color/Radius、boxShadow、opacity、filter、transform(2D only)
重要限制:
- 預設 display 是
flex,不是 block — 每個 div 都是 flex container
- 不支援
calc()
- 不支援
z-index(SVG 按文件順序繪製)
- 不支援 3D transforms
- 不支援 WOFF2 字型 — 只支援 TTF、OTF、WOFF
字型載入
const FONTS = [
{ name: 'Inter', data: fs.readFileSync('./fonts/Inter-Regular.ttf'), weight: 400, style: 'normal' },
{ name: 'Inter', data: fs.readFileSync('./fonts/Inter-Bold.ttf'), weight: 700, style: 'normal' },
];
await satori(element, {
fonts: FONTS,
loadAdditionalAsset: async (code, segment) => {
if (code === 'emoji') return `data:image/svg+xml;base64,...`;
return loadCJKFont(code);
},
});
規則:
- 只支援 TTF、OTF、WOFF(不支援 WOFF2)
- 字型資料必須是
ArrayBuffer 或 Buffer
- CJK 文字加
lang 屬性:<div lang="zh-TW">
圖片處理
{ type: 'img', props: { src: 'https://...', width: 200, height: 200 } }
const avatarBase64 = Buffer.from(
await fetch(url).then(r => r.arrayBuffer())
).toString('base64');
{ type: 'img', props: { src: `data:image/jpeg;base64,${avatarBase64}`, width: 200, height: 200 } }
務必指定 width/height,否則版面不可預測。
OG Image 尺寸標準
| 平台 | 建議尺寸 | 比例 |
|---|
| Open Graph(Facebook、Threads、LinkedIn) | 1200 x 630 | ~1.91:1 |
| Twitter summary_large_image | 1200 x 628 | ~1.91:1 |
統一用 1200 x 630。
Astro 整合(Build-time 產生)
import type { APIRoute, GetStaticPaths } from 'astro';
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import fs from 'node:fs';
const FONTS = [
{ name: 'Inter', data: fs.readFileSync('./public/fonts/Inter-Regular.ttf'), weight: 400 as const, style: 'normal' as const },
];
export const getStaticPaths: GetStaticPaths = async () => {
return users.map(user => ({ params: { slug: user.username }, props: { user } }));
};
export const GET: APIRoute = async ({ props }) => {
const svg = await satori(
{ type: 'div', props: { style: { }, children: props.user.name } },
{ width: 1200, height: 630, fonts: FONTS }
);
const png = new Resvg(svg, {
fitTo: { mode: 'width', value: 1200 },
font: { loadSystemFonts: false },
}).render().asPng();
return new Response(png, { headers: { 'Content-Type': 'image/png' } });
};
astro build 時自動為每個 static path 產生 PNG 檔案。
Meta Tags 設定
<meta property="og:title" content="台灣 GitHub 開發者排行榜" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://example.com/og/default.png" />
<meta property="og:url" content="https://example.com/" />
<meta property="og:description" content="台灣 GitHub 開發者 3D 視覺化排行" />
<meta property="og:site_name" content="GitStar" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:type" content="image/png" />
<meta property="og:locale" content="zh_TW" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="台灣 GitHub 開發者排行榜" />
<meta name="twitter:description" content="台灣 GitHub 開發者 3D 視覺化排行" />
<meta name="twitter:image" content="https://example.com/og/default.png" />
要點:
- Image URL 必須是絕對路徑
- 指定
og:image:width 和 og:image:height 避免爬蟲需要抓圖片判斷尺寸
- Twitter 用
summary_large_image 顯示大圖
- 用 PNG 格式(文字較清晰)
效能建議(大量產生時)
- 字型全域載入一次,不要每次重新讀取
- 遠端圖片預先 fetch 轉 base64
- resvg 設定
loadSystemFonts: false
- Build time 產生(SSG)而非 on-demand
- 內容未變則跳過重新產生
- JSX 結構保持簡單,深層巢狀會拖慢
- 多張圖片用
Promise.all 並行(注意記憶體)
常見陷阱
- 預設 display 是
flex 不是 block
- 不支援 WOFF2 字型
- 不支援
calc()、z-index、3D transforms
- img 未指定 width/height 導致版面異常
- 遠端圖片網路問題導致 build 失敗 → 預先 fetch + fallback