---
title: Generate OG images for Astro sites
description: Use Browser Run to automatically generate Open Graph social preview images for your Astro site pages.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/browser-run/llms.txt  
> Use this file to discover all available pages before exploring further. 

[Skip to content](#%5Ftop) 

# 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 Run 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 Run to screenshot that page as a PNG.
3. Serve the generated images to social media crawlers.

## Prerequisites

* A Cloudflare account with [Browser Run enabled](https://developers.cloudflare.com/browser-run/get-started/#quick-actions)
* An Astro site deployed on [Cloudflare Workers](https://developers.cloudflare.com/workers/framework-guides/web-apps/astro/)
* Basic familiarity with Astro and Cloudflare Workers

## 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`.

Note

This tutorial assumes your markdown posts have frontmatter fields for `title`, `slug`, and optionally `author`. For example:

YAML

```
---title: "My First Post"slug: "my-first-post"author: "John Doe"---
```

Adjust the `readPosts()` function in the script to match your frontmatter structure.

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

Terminal window

```
# For Cloudflare Workersnpx 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 Cloudflare Browser Run Quick Actions.

Create `scripts/generate-social-cards.ts`:

TypeScript

```
import {  existsSync,  mkdirSync,  readdirSync,  readFileSync,  writeFileSync,} from "fs";import { join } from "path";
// Configurationconst BASE_URL = "https://your-site.com"; // Your deployed site URLconst CF_API = "https://api.cloudflare.com/client/v4/accounts";const OUTPUT_DIR = "public/social-cards"; // Output directory for generated imagesconst 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();
```

Set your Cloudflare credentials as environment variables:

Terminal window

```
export CF_ACCOUNT_ID=your_account_idexport CF_API_TOKEN=your_api_token
```

Note

Browser Run has [rate limits](https://developers.cloudflare.com/browser-run/limits/) that vary by plan. The script includes a 200ms delay between requests to help stay within these limits. For large sites, you may need to run the script in batches.

Run the script to generate images:

Terminal window

```
# Generate new images onlybun scripts/generate-social-cards.ts
# Regenerate all imagesbun 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.astroconst { 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 Workersnpx wrangler deploy
```

Use these tools to verify your OG images render correctly:

* [Facebook Sharing Debugger ↗](https://developers.facebook.com/tools/debug/)
* [Twitter Card Validator ↗](https://cards-dev.twitter.com/validator)
* [LinkedIn Post Inspector ↗](https://www.linkedin.com/post-inspector/)

## 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 Run. When you share a link on social media, crawlers will fetch the generated image from the static path.

From here, you can:

* Customize your template with [custom fonts](#use-custom-fonts), [Tailwind CSS](#add-tailwind-css), or [background images](#add-a-background-image).
* Add cache invalidation logic to regenerate images when post content changes.
* Use [Cloudflare Images](https://developers.cloudflare.com/images/) or [Image Resizing](https://developers.cloudflare.com/images/optimization/transformations/overview/) for additional optimization.

## Related resources

* [Browser Run documentation](https://developers.cloudflare.com/browser-run/)
* [R2 storage](https://developers.cloudflare.com/r2/)
* [Cloudflare Images](https://developers.cloudflare.com/images/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/browser-run/how-to/og-images-astro/#page","headline":"Generate OG images for Astro sites · Cloudflare Browser Run docs","description":"Use Browser Run to automatically generate Open Graph social preview images for your Astro site pages.","url":"https://developers.cloudflare.com/browser-run/how-to/og-images-astro/","inLanguage":"en","image":"https://developers.cloudflare.com/dev-products-preview.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/browser-run/","name":"Browser Run"}},{"@type":"ListItem","position":3,"item":{"@id":"/browser-run/how-to/","name":"Tutorials"}},{"@type":"ListItem","position":4,"item":{"@id":"/browser-run/how-to/og-images-astro/","name":"Generate OG images for Astro sites"}}]}
```
