🎨
概念 #OGP #Satori #絵文字 #Twemoji 📚 ブログサイト構築

ブログサイト構築 — Satori で絵文字を描画する

Satori はテキストフォントで絵文字を描画できない。Twemoji SVG を img 要素として埋め込む解決策と、コードポイント変換・キャッシュ設計の実装メモ

問題:絵文字が□(豆腐)になる

Satori で絵文字をテキストとして描画すると、フォントに絵文字グリフが含まれていないため □(置換文字) として表示される。

// これは動かない
{ type: 'div', props: { children: '🚀' } }
→ OGP画像に □ が出力される

原因

Satori はフォントデータを直接受け取って文字を描画する。 絵文字(U+1F600 以降の範囲)は通常の Latin / CJK フォントには含まれない。 Noto Sans JP のような日本語フォントをいくら追加しても絵文字は描画できない。

解決策:Twemoji SVG を img 要素で埋め込む

Twemoji は Twitter が OSS として公開した絵文字 SVG セット。 絵文字の Unicode コードポイントに対応する SVG ファイルが CDN で公開されている。

🚀 → U+1F680 → https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/1f680.svg

Satori の img 要素は data:image/svg+xml の Data URL をサポートしているため、 SVG を fetch して Data URL に変換し img として渡すことで絵文字を描画できる。

// 1. 絵文字 → コードポイント変換
function emojiToCodePoint(emoji: string): string {
  return [...emoji]
    .map(c => c.codePointAt(0)!)
    .filter(cp => cp !== 0xfe0f)   // Variation Selector-16 を除去
    .map(cp => cp.toString(16))
    .join('-');
}
// '🚀' → '1f680'

// 2. Twemoji SVG を fetch して Data URL に変換
const url = `https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/${codePoint}.svg`;
const svg = await fetch(url).then(r => r.text());
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;

// 3. Satori の img 要素として渡す
{ type: 'img', props: { src: dataUrl, width: 80, height: 80,
    style: { width: '80px', height: '80px' } } }

Variation Selector-16(U+FE0F)の除去

絵文字には Variation Selector-16(U+FE0F)が付く場合がある。 これは「この文字を絵文字として描画せよ」という制御文字で、Twemoji のファイル名には含まれない。

'☁️' → ['☁'(U+2601), '️'(U+FE0F)]
→ フィルタ後: '2601'
→ https://.../assets/svg/2601.svg ← 正しい

ゼロ幅結合子(ZWJ)シーケンス

複数の絵文字を ZWJ(U+200D)で結合した複合絵文字(例:👨‍💻)も存在する。

'👨‍💻' → ['👨'(1F468), ZWJ(200D), '💻'(1F4BB)]
→ '1f468-200d-1f4bb'
→ https://.../assets/svg/1f468-200d-1f4bb.svg

Variation Selector と異なり ZWJ はコードポイントに含める必要がある。 フィルタ対象は U+FE0F のみ。

キャッシュ設計

ビルド中に同じ絵文字を複数記事で使う場合、毎回 fetch すると非効率。 モジュールスコープの Map でキャッシュする。

const emojiCache = new Map<string, string>();

async function loadEmojiDataUrl(emoji: string): Promise<string | null> {
  const cp = emojiToCodePoint(emoji);
  if (emojiCache.has(cp)) return emojiCache.get(cp)!;

  try {
    const url = `https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/${cp}.svg`;
    const res = await fetch(url);
    if (!res.ok) return null;
    const svg = await res.text();
    const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
    emojiCache.set(cp, dataUrl);
    return dataUrl;
  } catch {
    return null;
  }
}

fetch 失敗時は null を返し、呼び出し側で絵文字なしにフォールバックする。

なぜ絵文字フォントを使わないのか

Noto Emoji などの絵文字専用フォントを Satori に渡す方法もある。 しかし Noto Color Emoji は 20MB 超のフォントファイルであり、 ビルドごとに fetch すると著しく遅くなる。

Twemoji SVG は 1 絵文字あたり数 KB で済み、記事ごとに 1 種類しか使わないため fetch コストが低い。

方法ファイルサイズ対応絵文字実装複雑さ
絵文字フォント(Noto Color Emoji)20MB+全絵文字
Twemoji SVG(本実装)数KB / 絵文字Twitter 絵文字セット

関連