🏃♂️
SvelteKit で OGP 画像を自動生成する
SvelteKit で OGP 画像を自動生成する
はじめに
最近、ビジネスアイデアラボ というアプリを作りました。このアプリでは、AI がアイデアを評価したり、市場調査・競合調査をしてくれます。各ポストにはユニークな URL が振られており、内容も異なります。これらのポストを Facebook や X などの外部サイトに公開する際に、内容に基づいた OGP(Open Graph Protocol)画像があれば理想的です。
しかし、ポスト毎に静的な画像を作成するのは手間がかかります。そこで、自動で OGP 画像を生成する方法を探りました。この記事では、SvelteKit で OGP 画像を自動生成する方法について解説します。
画像生成の流れ
- 画像生成コードの作成
- 画像生成 API の作成
- 各コンテンツの OGP として設定
1. 画像生成コードの作成
画像生成には以下のライブラリを使用します:
- satori
- sharp
- Google Font API
Vercel 製の satori を選択し、sharp は画像処理に使用します。フォントは Google Font API を利用します。
画像生成コード(全体)
// src/lib/generateOGPImage.ts
import satori, { type SatoriOptions } from "satori";
import sharp from "sharp";
export const generateOgpImage = async (
title: string,
width: number,
height: number
) => {
if (!process.env.GOOGLE_FONTS_API_KEY) {
throw new Error("GOOGLE_FONTS_API_KEY is not set");
}
// Fetch Google Font
const endpoint = new URL("https://www.googleapis.com/webfonts/v1/webfonts");
endpoint.searchParams.set("family", "Noto Sans JP");
endpoint.searchParams.set("key", process.env.GOOGLE_FONTS_API_KEY ?? "");
try {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const info = await response.json();
console.log("API Response:", JSON.stringify(info, null, 2));
if (!info.items || info.items.length === 0) {
throw new Error("No font items found in the API response");
}
const fontItem = info.items[0];
if (!fontItem.files || !fontItem.files.regular) {
throw new Error("Regular font file not found in the API response");
}
const fontUrl = fontItem.files.regular;
const fontResponse = await fetch(fontUrl, { cache: "no-cache" });
if (!fontResponse.ok) {
throw new Error(`Failed to fetch font file: ${fontResponse.status}`);
}
const fontBuffer = await fontResponse.arrayBuffer();
const options: SatoriOptions = {
width,
height,
fonts: [
{
name: "Noto Sans JP",
data: fontBuffer,
weight: 400,
style: "normal",
},
],
};
const svg = await satori(
{
type: "div",
props: {
style: {
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0ea5e9",
backgroundImage:
"linear-gradient(to bottom right, #0ea5e9, #a855f7, #ec4899)",
fontSize: 64,
fontWeight: 700,
color: "white",
textAlign: "center",
padding: "40px",
},
children: [
{
type: "div",
props: {
children: title,
style: {
textShadow: "0 2px 4px rgba(0,0,0,0.2)",
},
},
},
],
},
},
options
);
const png = await sharp(Buffer.from(svg)).png().toBuffer();
return png;
} catch (error) {
console.error("Error in generateOgpImage:", error);
throw error;
}
};
2. 画像生成 API の作成
/posts/ogp/[title].png
というパスで API を準備します。Twitter と Facebook で推奨サイズが異なるため、/ogp/small/[title].png
と /ogp/large/[title].png
のように分けるのも良いでしょう。
画像生成 API(全体)
// src/routes/posts/ogp/[title].png/+server.ts
import { generateOgpImage } from "$lib/generateOgpImage";
import type { RequestHandler } from "@sveltejs/kit";
export const GET: RequestHandler = async ({ params }) => {
const { title } = params;
const width = 1200;
const height = 630;
const png = await generateOgpImage(
title ?? "Giants | BreakAI | AI for Business",
width,
height
);
return new Response(png, {
headers: {
"Content-Type": "image/png",
},
});
};
3. 各コンテンツの OGP として設定
SvelteKit で各コンテンツに OGP を設定するには、<svelte:head>
を使用します。
OGP 設定コード
<svelte:head>
<title>
{data.post.title} | {$LL.POSTPAGE.SITE_TITLE({ title: data.post.title })}
</title>
<meta name="description" content="{data.post.content}" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="article" />
<meta property="og:url"
content={`https://new-giants.breakai.ai/posts/${data.post.id}`} />
<meta property="og:title" content="{data.post.title}" />
<meta property="og:description" content="{data.post.content}" />
<meta property="og:image"
content={`https://new-giants.breakai.ai/posts/ogp/large/${encodeURIComponent(data.post.title
?? 'breakai')}.png`} />
<meta property="og:image:alt" content="{data.post.title}" />
<!-- Open Graph / Facebook - Multiple image sizes -->
<meta
property="og:image"
content="https://new-giants.breakai.ai/posts/ogp/large/${encodeURIComponent(
data.post.title ?? 'breakai'
)}.png"
/>
<meta property="og:image:width" content="1423" />
<meta property="og:image:height" content="771" />
<meta
property="og:image"
content="https://new-giants.breakai.ai/posts/ogp/small/${encodeURIComponent(
data.post.title ?? 'breakai'
)}.png"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="{$LL.META.TWITTER_CREATOR()}" />
<meta name="twitter:creator" content="{$LL.META.TWITTER_CREATOR()}" />
<meta name="twitter:title" content="{data.post.title}" />
<meta name="twitter:description" content="{data.post.content}" />
<meta name="twitter:image"
content={`https://new-giants.breakai.ai/posts/ogp/large/${encodeURIComponent(data.post.title
?? 'breakai')}.png`} />
<meta name="twitter:image:width" content="1423" />
<meta name="twitter:image:height" content="771" />
<meta name="twitter:image"
content={`https://new-giants.breakai.ai/posts/ogp/small/${encodeURIComponent(data.post.title
?? 'breakai')}.png`} />
<meta name="twitter:image:width" content="1200" />
<meta name="twitter:image:height" content="630" />
<meta name="twitter:card" content="{$LL.META.TWITTER_CARD()}" />
<meta name="twitter:site" content="{$LL.META.TWITTER_CREATOR()}" />
<meta name="twitter:creator" content="{$LL.META.TWITTER_CREATOR()}" />
<meta name="twitter:title" content="{data.post.title}" />
<meta name="twitter:description" content="{data.post.content}" />
</svelte:head>
パフォーマンス
Vercel 上にデプロイした場合、画像生成に 1-2 秒程度かかります。パフォーマンス向上のために、一度生成した画像をキャッシュすることをおすすめします。
ekusiadadus ~ 01:05
curl -w"time_total: %{time_total}\n" "https://new-giants.breakai.ai/posts/ogp/How%20To%20Perfectly%20Pitch%20Your%20Seed%20Stage%20Startup%20With%20Y%20Combinator's%20Michael%20Seibel.png"
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
time_total: 1.204007
Discussion