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 dev通过访问 http://localhost:4321/social-card?title=My%20Blog%20Post&author=Omar 在本地测试。
继续之前,部署站点以确保 /social-card 路由已上线:
# For Cloudflare Workers
npx wrangler deploy将下面脚本中的 BASE_URL 更新为与已部署站点 URL 匹配。
在 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"
}
}更新博客文章布局以引用生成的图像:
---
// 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 进行额外优化。