---
title: Generate OG images for Astro sites
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/browser-run/how-to/og-images-astro.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

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


```

Explain Code

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 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 Cloudflare Browser Run Quick Actions.

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 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();


```

Explain Code

Set your Cloudflare credentials as environment variables:

Terminal window

```

export CF_ACCOUNT_ID=your_account_id

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


```

Explain Code

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

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


```

Explain Code

### 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/transform-images/) 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":"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"}}]}
```
