ウェブサイトでOG画像を生成する際、フォント選択はブランドアイデンティティを表現する重要な要素です。このチュートリアルでは、Vercel OG Image Generationを使用して動的OG画像にローカルフォントを読み込み、適用する方法を解説します。
Vercel OGとフォントの基本
Vercel OGは、JSXを使用してOpen Graph画像を動的に生成するパワフルなライブラリです。カスタムフォントはOG画像のデザインをブランドに合わせるために重要な要素となります。
Vercel OGでフォントを使用するには主に3つの方法があります:
- 組み込みのシステムフォント
- カスタムフォントファイル(.ttf, .otfなど)を直接読み込む
- Googleフォントを使用する
今回はローカルフォントを使用する方法に焦点を当てます。これは、HonoXプロジェクトで最も安定した結果を得られる方法です。
ローカルフォントの設定
1. フォントファイルの準備
まず、使用したいフォントファイルをプロジェクトに追加する必要があります。例えば、Noto Sans JPとNoto Serif JPフォントを使用する場合:
- Googleフォントサイトからフォントファイル(.ttfまたは.otf)をダウンロード
- プロジェクト内の適切なディレクトリ(例:
public/fonts/
)に配置
public/
fonts/
Noto_Sans_JP/
static/
NotoSansJP-Regular.ttf
NotoSansJP-Bold.ttf
...
Noto_Serif_JP/
static/
NotoSerifJP-Regular.ttf
...
2. フォントファイルの読み込み
次に、OG画像生成のためにフォントファイルを読み込みます:
// app/utils/og.tsx または同様のファイル
import fs from "node:fs";
import path from "node:path";
import { ImageResponse } from "@vercel/og";
// ローカルフォントパスの指定
const notoSansJpRegularPath = path.resolve(
"./public/fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf"
);
const notoSansJpBoldPath = path.resolve("./public/fonts/Noto_Sans_JP/static/NotoSansJP-Bold.ttf");
const notoSerifJpRegularPath = path.resolve(
"./public/fonts/Noto_Serif_JP/static/NotoSerifJP-Regular.ttf"
);
// フォントファイルの読み込み
const notoSansJpRegularFont = fs.readFileSync(notoSansJpRegularPath);
const notoSansJpBoldFont = fs.readFileSync(notoSansJpBoldPath);
const notoSerifJpRegularFont = fs.readFileSync(notoSerifJpRegularPath);
このコードは、フォントファイルのパスを解決し、それらをバイナリデータとして読み込みます。
OG画像生成にフォントを適用する
フォントを読み込んだら、ImageResponseに適用します:
// OG画像生成関数の例
export function generateOgImage(title: string, description: string): ImageResponse {
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#f6f6f6",
fontFamily: "'Noto Sans JP', sans-serif",
}}
>
<h1 style={{ fontSize: 64, fontWeight: 700 }}>{title}</h1>
<p style={{ fontSize: 32 }}>{description}</p>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans JP",
data: notoSansJpRegularFont,
style: "normal",
weight: 400,
},
{
name: "Noto Sans JP",
data: notoSansJpBoldFont,
style: "normal",
weight: 700,
},
{
name: "Noto Serif JP",
data: notoSerifJpRegularFont,
style: "normal",
weight: 400,
},
],
}
);
}
フォント設定の解説
ImageResponse
の第2引数のoptionsオブジェクトにはfonts
配列を指定します:
fonts: [
{
name: "Noto Sans JP", // CSS font-familyで使用する名前
data: notoSansJpRegularFont, // フォントのバイナリデータ
style: "normal", // フォントスタイル
weight: 400, // フォントウェイト
},
// 複数のウェイトやスタイルを指定できる
{
name: "Noto Sans JP",
data: notoSansJpBoldFont,
style: "normal",
weight: 700,
},
// 別のフォントファミリーも追加できる
{
name: "Noto Serif JP",
data: notoSerifJpRegularFont,
style: "normal",
weight: 400,
},
];
実装例:HonoXでの実際の使用例
HonoXプロジェクトでの実際の実装例を見てみましょう。このブログサイトでは以下のように実装しています:
1. ヘルパー関数と共通ユーティリティ
app/utils/og.tsx
ファイルには、フォント読み込みと複数のOG画像生成関数があります:
// app/utils/og.tsx
import { ImageResponse } from "@vercel/og";
import fs from "node:fs";
import path from "node:path";
import { LocalizedContent } from "./locale";
// ローカルフォントの読み込み - 可変フォントではなく静的ファイルを使用
const notoSansJpRegularPath = path.resolve(
"./public/fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf"
);
const notoSansJpBoldPath = path.resolve("./public/fonts/Noto_Sans_JP/static/NotoSansJP-Bold.ttf");
const notoSerifJpRegularPath = path.resolve(
"./public/fonts/Noto_Serif_JP/static/NotoSerifJP-Regular.ttf"
);
// フォントファイルの読み込み
const notoSansJpRegularFont = fs.readFileSync(notoSansJpRegularPath);
const notoSansJpBoldFont = fs.readFileSync(notoSansJpBoldPath);
const notoSerifJpRegularFont = fs.readFileSync(notoSerifJpRegularPath);
// サイトアイコンのヘルパー関数
function getSiteIconBase64(): string {
try {
const iconPath = path.resolve("./public/icon.png");
const iconData = fs.readFileSync(iconPath);
return `data:image/png;base64,${iconData.toString("base64")}`;
} catch (error) {
console.error("Failed to load site icon:", error);
return ""; // アイコンが読み込めない場合は空文字を返す
}
}
// OG画像生成関数
export function generateContentOgImage(
locale: string,
localizedContent: LocalizedContent,
options: {
title: string;
category?: string;
categoryIcon?: string;
tags?: string[];
contentType?: "blog" | "idea";
}
): ImageResponse {
const { title, category, categoryIcon, tags = [], contentType = "blog" } = options;
// JSXコンテンツとスタイリング...
return new ImageResponse(
(
<div
style={{
// スタイルの設定...
fontFamily: "'Noto Sans JP', sans-serif",
}}
>
{/* JSX内容 */}
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans JP",
data: notoSansJpRegularFont,
style: "normal",
weight: 400,
},
{
name: "Noto Sans JP",
data: notoSansJpBoldFont,
style: "normal",
weight: 700,
},
{
name: "Noto Serif JP",
data: notoSerifJpRegularFont,
style: "normal",
weight: 400,
},
],
}
);
}
2. ルート定義でのフォント使用
app/routes/[locale]/blogs/[slug]/og.webp.tsx
などのOGイメージルートで、上記のヘルパー関数を使います:
// app/routes/[locale]/blogs/[slug]/og.webp.tsx
import { createRoute } from "honox/factory";
import { ssgParams } from "hono/ssg";
import { getMessage, assertValidLocale } from "../../../../utils/locale";
import { generateContentOgImage } from "../../../../utils/og";
export default createRoute(
ssgParams(() => [
/* ... */
]),
(c) => {
const locale = c.req.param("locale");
const slug = c.req.param("slug");
try {
// ロケールのバリデーション
assertValidLocale(locale);
// ブログ記事のデータ取得
const post = getBlogPost(locale, slug);
// ローカライズされたコンテンツの取得
const localizedContent = getMessage(locale);
const { frontmatter } = post;
// OG画像を生成
return generateContentOgImage(locale, localizedContent, {
title: frontmatter.title,
category: frontmatter.category,
categoryIcon: getCategoryIcon(frontmatter.category),
tags: frontmatter.tags,
contentType: "blog",
});
} catch (error) {
return c.notFound();
}
}
);
フォント選択のベストプラクティス
1. 静的ファイルの使用
可変フォント(Variable Font)ではなく、静的ウェイト(Regular、Boldなど)のフォントファイルを使用します:
// 良い例: 静的ウェイトのフォントを使用
const notoSansJpRegularPath = path.resolve(
"./public/fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf"
);
// 避けるべき例: 可変フォントを使用
const notoSansJpVariablePath = path.resolve(
"./public/fonts/Noto_Sans_JP/NotoSansJP-VariableFont_wght.ttf"
);
これは、Vercel OGが可変フォントよりも静的ウェイトのフォントで最適に動作するためです。
2. 必要なウェイトとスタイルのみを読み込む
パフォーマンスのために、実際に使用するウェイトとスタイルのみを読み込みましょう:
// 使用するウェイトのみを読み込む
const notoSansJpRegularFont = fs.readFileSync(notoSansJpRegularPath);
const notoSansJpBoldFont = fs.readFileSync(notoSansJpBoldPath);
3. Base64エンコード画像の使用
ロゴやアイコンなどの画像はBase64エンコードして埋め込むことができます:
// 画像をBase64エンコードするヘルパー関数
function getImageBase64(imagePath: string): string {
try {
const imageData = fs.readFileSync(path.resolve(imagePath));
return `data:image/png;base64,${imageData.toString("base64")}`;
} catch (error) {
return "";
}
}
// JSXでの使用
<img src={getImageBase64("./public/icon.png")} width={100} height={100} alt="アイコン" />;
パフォーマンス最適化
フォントファイルのサイズ最適化
フォントファイルを小さくするためのテクニックもあります:
- サブセットフォントの使用(必要な文字のみを含む)
- Webフォント用にフォントを最適化するツールの使用
エッジケースと注意点
1. フォントが見つからない場合の対応
フォントファイルが見つからない場合のエラーハンドリングを実装しましょう:
function loadFontWithFallback(fontPath: string, fallbackFontPath: string): Buffer {
try {
return fs.readFileSync(fontPath);
} catch (error) {
console.error(`Failed to load font from ${fontPath}, using fallback`);
try {
return fs.readFileSync(fallbackFontPath);
} catch {
throw new Error("Failed to load both primary and fallback fonts");
}
}
}
2. 国際化対応
多言語サイトでは、言語ごとに適切なフォントを選択することが重要です:
function getFontForLocale(locale: string) {
switch (locale) {
case "ja":
return {
fontFamily: "'Noto Sans JP', sans-serif",
fontData: [notoSansJpRegularFont, notoSansJpBoldFont],
};
case "zh":
return {
fontFamily: "'Noto Sans SC', sans-serif",
fontData: [notoSansSCRegularFont, notoSansSCBoldFont],
};
default:
return {
fontFamily: "'Inter', sans-serif",
fontData: [interRegularFont, interBoldFont],
};
}
}
Vercel OGのフォント関連の制限事項
Vercel OGを使う際には、いくつかの制限があることを知っておくと良いでしょう:
- 複数の子要素を持つdivにはdisplay属性が必要:
display: "flex"
またはdisplay: "none"
を明示的に指定
// 良い例
<div style={{ display: "flex" }}>
<child1 />
<child2 />
</div>
// 悪い例 - エラーになる可能性あり
<div>
<child1 />
<child2 />
</div>
-
CSSの制限: サポートされていないCSSプロパティがある
-
WebPの制限: Base64埋め込み画像にはWebP形式ではなくPNGを使用する方が安全
.webp.tsx ファイル拡張子の重要性
OG画像ルートには.webp.tsx
ファイル拡張子を使用することが重要です:
app/routes/[locale]/blogs/[slug]/og.webp.tsx
この拡張子は単なる規約ではなく、ビルドプロセスが正しく画像を処理するために必要です。
ビルド後のWebP変換の実装
Vercel OGは内部的にPNGを生成しますが、これをWebPに変換することでパフォーマンスを向上できます:
// scripts/convert-webp.js
import sharp from "sharp";
import fs from "fs";
import path from "path";
import glob from "glob";
// PNGファイルをWebPに変換するスクリプト
const convertToWebP = async () => {
const files = glob.sync("dist/**/*.png");
for (const file of files) {
const outputPath = file.replace(".png", ".webp");
await sharp(file).webp({ quality: 90 }).toFile(outputPath);
console.log(`Converted ${file} to ${outputPath}`);
}
};
convertToWebP().catch(console.error);
まとめ
Vercel OGでローカルフォントを使用することで、動的に生成されるOG画像に対して、一貫性のある高品質なタイポグラフィを実現できます。
この記事で学んだ主なポイント:
- ローカルフォントの設定: プロジェクトにフォントファイルを追加し、fs.readFileSyncで読み込む
- フォント情報の定義: フォント名、スタイル、ウェイトを適切に指定する
- パフォーマンス最適化: 必要なフォントのみを読み込み、キャッシュを活用する
- 国際化対応: 言語に応じた適切なフォントを選択する
- Webフォントの制限を把握する: Vercel OGの制限事項を理解し、対策を講じる
これらのテクニックを組み合わせることで、ブランドに一貫したプロフェッショナルなOG画像を生成できるようになります。