Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Astro サイト向けに OG 画像を生成する

最終更新 Markdown で表示Agent セットアップ

Open Graph(OG)画像は、ソーシャルメディアでリンクを共有したときに表示されるプレビュー画像です。ブログ記事ごとに手作業で作る代わりに、Cloudflare Browser Run を使い、Astro テンプレートからブランド付きのソーシャルプレビュー画像を自動生成できます。

このチュートリアルでは、次を行います。

  1. OG 画像のデザインを描画する Astro ページを作成します。
  2. Browser Run でそのページをスクリーンショットし、PNG として保存します。
  3. 生成した画像をソーシャルメディアのクローラーに配信します。

前提条件

1. OG 画像テンプレートを作成する

OG 画像のデザインを描画する Astro ルートを作成します。このページが、画像レイアウトの唯一のソースになります。

src/pages/social-card.astro を作成します。

---
export const prerender = false;

const title = Astro.url.searchParams.get("title") || "Untitled";
const image = Astro.url.searchParams.get("image");
const author = Astro.url.searchParams.get("author");
---

<html>
	<head>
		<meta charset="utf-8" />
		<style>
			* {
				margin: 0;
				padding: 0;
				box-sizing: border-box;
			}
			body {
				width: 1200px;
				height: 630px;
				display: flex;
				flex-direction: column;
				justify-content: flex-end;
				padding: 60px;
				font-family: system-ui, sans-serif;
				background: linear-gradient(135deg, #f38020 0%, #f9a825 100%);
				color: white;
			}
			.title {
				font-size: 64px;
				font-weight: bold;
				line-height: 1.1;
				margin-bottom: 24px;
			}
			.author {
				font-size: 24px;
				opacity: 0.9;
			}
			.logo {
				position: absolute;
				top: 60px;
				left: 60px;
				height: 40px;
			}
		</style>
	</head>
	<body>
		<img class="logo" src="/your-logo.png" alt="Your logo" />
		<h1 class="title">{title}</h1>
		{author && <p class="author">By {author}</p>}
	</body>
</html>

Astro の開発サーバーを起動して、テンプレートを確認します。

npm run dev

http://localhost:4321/social-card?title=My%20Blog%20Post&author=Omar を開いて、ローカルで確認します。

先に進む前にサイトをデプロイし、/social-card ルートが公開されていることを確認します。

# For Cloudflare Workers
npx wrangler deploy

次のスクリプトの BASE_URL を、デプロイしたサイトの URL に合わせて更新します。

2. ビルド時に OG 画像を生成する

Cloudflare Browser Run の Quick Actions を使い、Astro のビルド中にすべての OG 画像を生成します。

scripts/generate-social-cards.ts を作成します。

import {
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	writeFileSync,
} from "fs";
import { join } from "path";

// Configuration
const BASE_URL = "https://your-site.com"; // Your deployed site URL
const CF_API = "https://api.cloudflare.com/client/v4/accounts";
const OUTPUT_DIR = "public/social-cards"; // Output directory for generated images
const POSTS_DIR = "src/data/posts"; // Directory containing your markdown posts (adjust to match your project)

interface Post {
	slug: string;
	title: string;
	author?: string;
}

/** Extract a frontmatter field value from raw markdown content. */
function getFrontmatterField(content: string, field: string): string | null {
	const match = content.match(new RegExp(`^${field}:\\s*"?([^"\\n]+)"?`, "m"));
	return match ? match[1].trim() : null;
}

/**
 * Read all post files and return { slug, title, author }[].
 * This function scans the POSTS_DIR for markdown files, extracts frontmatter
 * fields (slug, title, author), and returns an array of post objects.
 * Falls back to filename for slug and slug for title if frontmatter is missing.
 */
function readPosts(): Post[] {
	if (!existsSync(POSTS_DIR)) return [];
	const files = readdirSync(POSTS_DIR).filter((f) => f.endsWith(".md"));
	return files.map((file) => {
		const raw = readFileSync(join(POSTS_DIR, file), "utf-8");
		const slug = getFrontmatterField(raw, "slug") ?? file.replace(/\.md$/, "");
		const title = getFrontmatterField(raw, "title") ?? slug;
		const author = getFrontmatterField(raw, "author") ?? undefined;
		return { slug, title, author };
	});
}

/**
 * Capture a screenshot using Cloudflare Browser Run Quick Actions
 */
async function captureScreenshot(
	accountId: string,
	apiToken: string,
	pageUrl: string,
): Promise<ArrayBuffer> {
	const endpoint = `${CF_API}/${accountId}/browser-rendering/screenshot`;

	const res = await fetch(endpoint, {
		method: "POST",
		headers: {
			Authorization: `Bearer ${apiToken}`,
			"Content-Type": "application/json",
		},
		body: JSON.stringify({
			url: pageUrl,
			viewport: { width: 1200, height: 630 }, // Standard OG image size
			gotoOptions: { waitUntil: "networkidle0" }, // Wait for page to fully load
		}),
	});

	if (!res.ok) {
		const text = await res.text();
		throw new Error(`Screenshot API returned ${res.status}: ${text}`);
	}

	return res.arrayBuffer();
}

async function main() {
	// Read credentials from environment variables
	const accountId = process.env.CF_ACCOUNT_ID;
	const apiToken = process.env.CF_API_TOKEN;

	if (!accountId || !apiToken) {
		console.error("Error: CF_ACCOUNT_ID and CF_API_TOKEN required");
		process.exit(1);
	}

	// Check if --force flag is passed to regenerate all images
	const force = process.argv.includes("--force");

	// Read posts from markdown files
	const posts = readPosts();

	if (posts.length === 0) {
		console.log("No posts found. Check your POSTS_DIR path.");
		process.exit(0);
	}

	console.log(`Found ${posts.length} posts to process\n`);

	// Ensure output directory exists
	mkdirSync(OUTPUT_DIR, { recursive: true });

	let generated = 0;
	let skipped = 0;

	// Generate social card for each post
	for (let i = 0; i < posts.length; i++) {
		const post = posts[i];
		const outPath = join(OUTPUT_DIR, `${post.slug}.png`);
		const label = `[${i + 1}/${posts.length}]`;

		// Skip if file exists and --force flag not set
		if (!force && existsSync(outPath)) {
			console.log(`${label} ${post.slug}.png — skipped (exists)`);
			skipped++;
			continue;
		}

		// Build URL with query parameters for the OG template
		const params = new URLSearchParams({
			title: post.title,
			author: post.author || "",
		});
		const url = `${BASE_URL}/social-card?${params}`;

		try {
			// Capture screenshot and save to file
			const png = await captureScreenshot(accountId, apiToken, url);
			writeFileSync(outPath, Buffer.from(png));
			console.log(`${label} ${post.slug}.png — done`);
			generated++;
		} catch (err) {
			console.error(`${label} ${post.slug}.png — failed:`, err);
		}

		// Rate limiting: small delay between requests
		if (i < posts.length - 1) {
			await new Promise((resolve) => setTimeout(resolve, 200));
		}
	}

	console.log(`\nDone. Generated: ${generated}, Skipped: ${skipped}`);
}

main();

Cloudflare の認証情報を環境変数として設定します。

export CF_ACCOUNT_ID=your_account_id
export CF_API_TOKEN=your_api_token

スクリプトを実行して画像を生成します。

# Generate new images only
bun scripts/generate-social-cards.ts

# Regenerate all images
bun scripts/generate-social-cards.ts --force

任意で、package.json のビルドスクリプトに追加します。

{
	"scripts": {
		"build": "bun scripts/generate-social-cards.ts && astro build"
	}
}

3. ページに OG メタタグを追加する

ブログ記事のレイアウトを更新し、生成した画像を参照します。

---
// src/layouts/BlogPost.astro
const { title, slug, author } = Astro.props;
const ogImageUrl = `/social-cards/${slug}.png`;
---

<html>
	<head>
		<meta property="og:title" content={title} />
		<meta property="og:image" content={ogImageUrl} />
		<meta property="og:image:width" content="1200" />
		<meta property="og:image:height" content="630" />
		<meta name="twitter:card" content="summary_large_image" />
		<meta name="twitter:image" content={ogImageUrl} />
	</head>
	<body>
		<slot />
	</body>
</html>

4. OG 画像を確認する

確認の前に、新しく生成したソーシャルカード画像を含めてサイトをデプロイします。

# For Cloudflare Workers
npx wrangler deploy

次のツールで、OG 画像が正しく表示されることを確認します。

テンプレートをカスタマイズする

背景画像を追加する

---
const title = Astro.url.searchParams.get("title") || "Untitled";
const image = Astro.url.searchParams.get("image");
---

<body style={image ? `background-image: url(${image})` : undefined}>
	<!-- content -->
</body>

カスタムフォントを使う

<head>
	<link
		href="https://fonts.googleapis.com/css2?family=Inter:wght@700&display=swap"
		rel="stylesheet"
	/>
	<style>
		body {
			font-family: "Inter", sans-serif;
		}
	</style>
</head>

Tailwind CSS を追加する

Astro サイトで Tailwind を使っている場合は、OG テンプレートでも使えます。

---
import "../styles/global.css";
---

<body
	class="flex h-[630px] w-[1200px] flex-col justify-end bg-gradient-to-br from-orange-500 to-amber-500 p-16 text-white"
>
	<h1 class="mb-6 text-6xl leading-tight font-bold">{title}</h1>
</body>

パフォーマンスの考慮点

画像の最適化

生成した画像を、追加の最適化のために Cloudflare Images または Image Resizing に通すことも検討してください。

const optimizedUrl = `https://your-domain.com/cdn-cgi/image/width=1200,format=auto/social-cards/${slug}.png`;

次のステップ

これで、Astro サイトは Browser Run を使って OG 画像を自動生成します。ソーシャルメディアでリンクを共有すると、クローラーは静的パスから生成済み画像を取得します。

ここから次のことができます。

関連リソース

役に立ちましたか?