跳转到内容
搜索文档

为 Astro 站点生成 OG 图像

最后更新 查看 MarkdownAgent 设置

Open Graph(OG)图像是在社交媒体上分享链接时出现的预览图像。无需为每篇博客文章手动创建这些图像,你可以使用 Cloudflare Browser Run 从 Astro 模板自动生成品牌化的社交预览图像。

在本教程中,你将:

  1. 创建渲染 OG 图像设计的 Astro 页面。
  2. 使用 Browser Run 将该页面截图为 PNG。
  3. 向社交媒体爬虫提供生成的图像。

前提条件

  • 已启用 Browser Run 的 Cloudflare 账户
  • 部署在 Cloudflare Workers 上的 Astro 站点
  • 熟悉 Astro 和 Cloudflare Workers 的基础知识

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 图像

在 Astro 构建过程中使用 Cloudflare Browser Run Quick Actions 生成所有 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 meta 标签

更新博客文章布局以引用生成的图像:

---
// 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 图像。在社交媒体上分享链接时,爬虫将从静态路径获取生成的图像。

从这里,你可以:

相关资源

这篇文档对您有帮助吗?