1603 文字
8 分
Astro の OGP 画像生成で日本語の改行を自然にする

はじめに#

ブログ記事を SNS でシェアしたとき、OGP 画像の有無でクリック率は大きく変わります。
Astro なら satori を使ってビルド時に自動生成するのが定番ですが、
Cloudflare Pages にデプロイする日本語ブログではいくつかの罠があります。

このブログで実際に踏んだ 2 つの問題と、その解決策を紹介します。

  1. sharp が Cloudflare のランタイムで動かない
  2. 日本語タイトルが文字単位で折返され、文節の途中で改行される

構成の全体像#

最終的に採用したパイプラインは以下の通りです。

flowchart LR
    A["getStaticPaths()"] --> B["satori<br/>HTML → SVG"]
    B --> C["resvg<br/>SVG → PNG"]
    C --> D["dist/og/slug.png"]

ポイントは export const prerender = true を宣言し、
ビルド時に全投稿分の OGP を静的ファイルとして出力する点です。ランタイムでは一切画像生成をしません。

src/pages/og/[slug].png.ts
export const prerender = true;
export async function getStaticPaths() {
const posts = await getCollection("posts");
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}

記事ページ側では、<meta property="og:image">/og/${slug}.png を指定しておけば、
ビルドで生成された静的ファイルがそのまま配信されます。

なぜ sharp ではダメだったのか#

Astro の OGP 画像生成で検索すると、多くの記事が satori + sharp の組み合わせを紹介しています。
僕も最初はこの構成でした。

ところが Cloudflare Pages では sharp がランタイムで動きません
sharp は C++ の native binding に依存しており、Cloudflare の V8 Isolate 環境では実行できないからです。

prerender: true を付ければビルド時(= Node.js 環境)で実行されるため sharp でも動きますが、
この宣言を忘れると Astro は SSR ルート扱いにします。
結果として本番デプロイ後に OGP エンドポイントが 5xx を返し、SNS のクローラーが画像を取得できなくなります。

解決策は sharp@resvg/resvg-js に置き換えることです。

import { Resvg } from "@resvg/resvg-js";
// satori が返した SVG 文字列を PNG に変換
const resvg = new Resvg(svg, {
fitTo: { mode: "width", value: 1200 },
});
const png = resvg.render().asPng();

@resvg/resvg-js も native Node binding ですが、
prerender: true と組み合わせることでビルド時にしか実行されないため、ランタイム非対応の問題を完全に回避できます。

なお、native binding を含むパッケージは Astro の server bundle に巻き込まれるとビルドが失敗します。
astro.config.mjs で明示的に除外する必要があります。

astro.config.mjs
vite: {
ssr: {
external: ["@resvg/resvg-js", "satori", "satori-html"],
},
},

日本語タイトルの改行問題#

ここまでで OGP 画像は生成できるようになりましたが、日本語ブログ特有の問題が残ります。

satori のテキストレイアウトエンジンは 文字単位 で折返しを判定します。
英語なら単語境界(スペース)で自然に切れますが、日本語にはスペースによる単語区切りがないため、
「渡す」が「渡」と「す」に分断されるような不自然な改行が発生しました。

ZWSP は使えない#

最初に試したのは zero-width space(U+200B)を文節境界に挿入して word-break: keep-all と組み合わせる方法でした。
CSS の仕様上これが正攻法ですが、
satori は ZWSP をグリフとして描画してしまい、□(tofu)が表示されるという問題がありました。

budoux で解決#

最終的に採用したのは、Google 製の日本語改行ライブラリ budoux です。
budoux は学習済みモデルで日本語の文節境界を推定し、自然な位置で分割してくれます。

アプローチは以下の通りです。

  1. budoux でタイトルを文節に分割する
  2. フォントサイズとコンテナ幅から 1 行あたりの最大文字幅を計算する
  3. 文節を貪欲法で行に詰め、上限を超えたら次の行へ送る
  4. 各行を個別の <div> として satori に渡す
import { loadDefaultJapaneseParser } from "budoux";
const jpParser = loadDefaultJapaneseParser();
function estimateWidth(text: string): number {
let w = 0;
for (const ch of text) {
w += ch.charCodeAt(0) > 0x7f ? 1 : 0.55;
}
return w;
}
function splitTitleIntoLines(title: string, fontSize: number): string[] {
const maxUnits = (CONTENT_WIDTH / fontSize) * 0.95;
const phrases = jpParser.parse(title);
const lines: string[] = [];
let current = "";
for (const phrase of phrases) {
const next = current + phrase;
if (estimateWidth(next) > maxUnits && current.length > 0) {
lines.push(current);
current = phrase;
} else {
current = next;
}
}
if (current) lines.push(current);
return lines;
}

estimateWidth では CJK 文字を幅 1.0、ASCII 文字を 0.55 として近似しています。
正確なグリフ幅測定ではないものの、実用上十分な精度で行折返しの判定ができます。

Before / After#

記事タイトルBeforeAfter
Claude Code にプロジェクト文脈を渡す 4 層構造...文脈を渡 / す 4 層構造...プロジェクト文脈を / 渡す 4 層構造
SubCutter - ブラウザ完結の固定費管理PWAを作った...完結の固定費 / 管理PWAを作った...ブラウザ完結の / 固定費管理PWAを作った

はまりポイントまとめ#

  1. prerender: true を忘れると本番で壊れる
    Cloudflare Pages + Astro の組み合わせでは、native binding を使うエンドポイントには必ず prerender: true を付ける。付け忘れると開発環境では動くのに本番で 5xx になる。

  2. satori は全要素に display: flex が必要
    satori のレイアウトエンジンは Yoga ベースで、すべての要素が Flexbox として評価される。display: flex を省略すると予期しないレイアウト崩れが起きる。

  3. satori-html の tagged template は値を HTML エスケープする
    html`...${dynamicHtml}...` と書くと dynamicHtml 内の <div> がエスケープされてしまう。動的に生成した HTML を挿入するには、raw 文字列として渡すヘルパーが必要。

まとめ#

  • Astro + Cloudflare Pages で OGP 画像を自動生成するなら、prerender: true + satori + @resvg/resvg-js がビルド時完結で安定する
  • 日本語ブログでは budoux を入れるだけで改行品質が劇的に改善 される。約 30 KB の依存を追加するだけで、全記事の OGP 画像が自然な文節区切りになる
  • satori 特有の制約(display: flex 必須、ZWSP の tofu 化、satori-html のエスケープ)は事前に知っておくとデバッグ時間を大幅に節約できる

まずは自分のブログの OGP エンドポイントに export const prerender = true が付いているか確認するところから始めるのがおすすめです。

Astro の OGP 画像生成で日本語の改行を自然にする
https://blog.c12o.net/posts/astro-og-image-generation/
作者
Seu (c12o)
公開日
2026-04-12
ライセンス
CC BY-NC-SA 4.0