← Browser Run / browser-run / how-to
Генерация OG-изображений для сайтов Astro
Изображения Open Graph (OG) представляют собой превью, которые появляются при публикации ссылки в социальных сетях. Вместо того чтобы создавать такие изображения вручную для каждой записи блога, можно использовать Cloudflare Browser Run для автоматической генерации фирменных превью на основе шаблона Astro.
В этом руководстве вы:
- Создайте страницу Astro, которая отрисовывает дизайн вашего OG-изображения.
- Используйте Browser Run, чтобы сделать снимок этой страницы в формате PNG.
- Предоставляйте сгенерированные изображения краулерам социальных сетей.
Предварительные требования
- Аккаунт Cloudflare с Browser Run включён
- Сайт на Astro, развёрнутый на Cloudflare Workers
- Базовое знакомство с Astro и Cloudflare Workers
1. Создание шаблона OG-изображения
Создайте маршрут Astro, который отрисовывает дизайн вашего OG-изображения. Эта страница служит источником достоверных данных о макете изображения.
Создайте 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-изображений во время сборки
Генерируйте все OG-изображения в процессе сборки Astro с помощью Quick Actions Cloudflare Browser Run.
Создайте 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 автоматически генерирует OG-изображения с помощью Browser Run. При публикации ссылки в социальных сетях краулеры получают сгенерированное изображение по статическому пути.
Отсюда можно:
- Настройте свой шаблон с помощью пользовательские шрифты, Tailwind CSS, или фоновые изображения.
- Добавьте логику инвалидации кеша, чтобы изображения пересоздавались при изменении содержимого записи.
- Используйте Cloudflare Images или Image Resizing для дополнительной оптимизации.