Open Graph(OG)画像は、ソーシャルメディアでリンクを共有したときに表示されるプレビュー画像です。ブログ記事ごとに手作業で作る代わりに、Cloudflare Browser Run を使い、Astro テンプレートからブランド付きのソーシャルプレビュー画像を自動生成できます。
このチュートリアルでは、次を行います。
- OG 画像のデザインを描画する Astro ページを作成します。
- Browser Run でそのページをスクリーンショットし、PNG として保存します。
- 生成した画像をソーシャルメディアのクローラーに配信します。
- Browser Run が有効 な Cloudflare アカウント
- Cloudflare Workers にデプロイした Astro サイト
- Astro と Cloudflare Workers の基本的な知識
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 devhttp://localhost:4321/social-card?title=My%20Blog%20Post&author=Omar を開いて、ローカルで確認します。
先に進む前にサイトをデプロイし、/social-card ルートが公開されていることを確認します。
# For Cloudflare Workers
npx wrangler deploy次のスクリプトの BASE_URL を、デプロイしたサイトの URL に合わせて更新します。
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"
}
}ブログ記事のレイアウトを更新し、生成した画像を参照します。
---
// 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>確認の前に、新しく生成したソーシャルカード画像を含めてサイトをデプロイします。
# 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>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 画像を自動生成します。ソーシャルメディアでリンクを共有すると、クローラーは静的パスから生成済み画像を取得します。
ここから次のことができます。
- カスタムフォント、Tailwind CSS、背景画像 でテンプレートをカスタマイズする。
- 投稿内容が変わったときに画像を再生成する、キャッシュ無効化のロジックを追加する。
- 追加の最適化に Cloudflare Images または Image Resizing を使う。