Skip to content
Cloudflare Docs

Generate OG images for Astro sites

Open Graph (OG) images are the preview images that appear when you share a link on social media. Instead of manually creating these images for every blog post, you can use Cloudflare Browser Rendering to automatically generate branded social preview images from an Astro template.

In this tutorial, you will:

  1. Create an Astro page that renders your OG image design.
  2. Use Browser Rendering to screenshot that page as a PNG.
  3. Serve the generated images to social media crawlers.

Prerequisites

1. Create the OG image template

Create an Astro route that renders your OG image design. This page serves as the source of truth for your image layout.

Create 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>

Start your Astro development server to test the template:

Terminal window
npm run dev

Test locally by visiting http://localhost:4321/social-card?title=My%20Blog%20Post&author=Omar.

Before proceeding, deploy your site to ensure the /social-card route is live:

Terminal window
# For Cloudflare Workers
npx wrangler deploy

Update the BASE_URL in the script below to match your deployed site URL.

2. Generate OG images at build time

Generate all OG images during the Astro build process using the Cloudflare Browser Rendering REST API.

Create scripts/generate-social-cards.ts:

TypeScript
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 Rendering REST API
*/
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();

Set your Cloudflare credentials as environment variables:

Terminal window
export CF_ACCOUNT_ID=your_account_id
export CF_API_TOKEN=your_api_token

Run the script to generate images:

Terminal window
# Generate new images only
bun scripts/generate-social-cards.ts
# Regenerate all images
bun scripts/generate-social-cards.ts --force

Optionally, add to your build script in package.json:

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

3. Add OG meta tags to your pages

Update your blog post layout to reference the generated images:

---
// 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. Test your OG images

Before testing, make sure to deploy your site with the newly generated social card images:

Terminal window
# For Cloudflare Workers
npx wrangler deploy

Use these tools to verify your OG images render correctly:

Customize the template

Add a background image

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

Use custom fonts

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

Add Tailwind CSS

If your Astro site uses Tailwind, you can use it in your OG template:

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

Performance considerations

Image optimization

Consider running generated images through Cloudflare Images or Image Resizing for additional optimization:

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

Next steps

Your Astro site now automatically generates OG images using Browser Rendering. When you share a link on social media, crawlers will fetch the generated image from the static path.

From here, you can: