You’ve just spent three hours polishing a highly technical article. You copy the link, proudly paste it on LinkedIn or Discord, and then… disaster strikes.

An old pixelated logo appears, or worse, an empty gray box. Your link is “sad”.

The click-through rate (CTR) of a link without a social image (OpenGraph) plummets. But opening Figma to create a thumbnail for every new article is a repetitive and time-consuming task. What if we let Astro handle it for us?

What is an OpenGraph image?

OpenGraph is a protocol originally created by Facebook to standardize how web pages are represented on social networks.

[Image of OpenGraph metadata functioning on social media platforms]

Technically, these are simple <meta> tags located in the <head> of your page:

<meta property="og:image" content="[https://vbesse.com/og/my-article.png](https://vbesse.com/og/my-article.png)" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="[https://vbesse.com/og/my-article.png](https://vbesse.com/og/my-article.png)" />

The challenge isn’t adding these tags, but creating the .png file that corresponds to the URL.

The Astro Approach: Endpoints

The magic of Astro is that it doesn’t just generate HTML. It can generate JSON, XML (for RSS feeds, as we’ve seen!), and even images on the fly using Endpoints.

Rather than creating an .astro page, we are going to create a .ts file in our routing folder.

1. Creating the Satori Generator

The industry standard for generating these images through code is called Satori (developed by Vercel). It converts HTML/CSS to SVG, which we then transform into a PNG.

Create a src/pages/en/og/[slug].png.ts file. This file will generate a unique image for each of your English blog posts.

import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
// (Mock imports for the example: satori and resvg-js)
import { generateImage } from '../../../utils/og-generator'; 

// 1. Astro lists all our English articles to know which images to generate
export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ id }) => id.startsWith('en/'));
  
  return posts.map((post) => {
    const cleanSlug = post.slug.replace('en/', '');
    return {
      params: { slug: cleanSlug },
      props: { title: post.data.title, description: post.data.description },
    };
  });
}

// 2. Astro creates the PNG file for each article
export const GET: APIRoute = async ({ props }) => {
  const { title, description } = props;
  
  // Our magic function that takes the title and returns a PNG buffer
  const pngBuffer = await generateImage(title, description);

  return new Response(pngBuffer, {
    headers: { 'Content-Type': 'image/png' },
  });
};

Design by Code

Thanks to Satori, you don’t need to learn a complex graphic language. You design your cover image using standard HTML flexbox and CSS!

Here is what the template passed to the generator looks like:

<div style={{ display: 'flex', flexDirection: 'column', width: '1200px', height: '630px', background: '#1a1a1a', color: 'white', padding: '80px' }}>
  <h1 style={{ fontSize: '64px', fontWeight: 'bold' }}>{title}</h1>
  <p style={{ fontSize: '32px', color: '#a1a1aa' }}>{description}</p>
  
  <div style={{ display: 'flex', marginTop: 'auto', alignItems: 'center' }}>
    <img src="[https://vbesse.com/avatar.jpg](https://vbesse.com/avatar.jpg)" width="80" height="80" style={{ borderRadius: '50%' }} />
    <span style={{ fontSize: '32px', marginLeft: '20px' }}>vbesse.com</span>
  </div>
</div>

Connecting the image to the Layout

Now that our images are generated in the /en/og/ folder, we just need to tell our article to go get them.

Remember our BlogPost.astro from the previous article. We simply need to pass it the correct URL:

---
// src/layouts/BlogPost.astro
import BaseHead from '../components/BaseHead.astro';
const { frontmatter, slug } = Astro.props;

// We dynamically build the link to our new English image
const ogImageUrl = new URL(`/en/og/${slug}.png`, Astro.site);
---

<html lang="en">
  <head>
    <BaseHead 
      title={frontmatter.title} 
      description={frontmatter.description} 
      image={ogImageUrl} 
    />
  </head>
  </html>

Conclusion

By investing an hour to set up Satori and an Astro endpoint, you free yourself entirely from the chore of image creation.

With every npm run build, Astro reads your articles, generates the corresponding PNG files with the right title, and injects them into your Meta tags.

Your links will never be sad again. And your readers will (finally) want to click on them.