Dynamic OpenGraph Images in Astro

If you’ve ever shared a link on social media, you know how critical OpenGraph (OG) images are. They’re the first thing people see – often before they even click.
Static OG images are fine as a start, but what if you want custom images for every blog post or content collection item?

For Astro there is astro-og-canvas, a nice and useful Astro plugin that utilizes Canvas to create dynamic OG images.

In this post, I’ll walk you through how to generate dynamic OG images for your Astro site, inspired by Aidan Kinzett’s excellent post.
I’ll also share some odds and learned lessons.

Step 1: Set Up `astro-og-canvas`

First, install the package:

npm install astro-og-canvas

Create a new file at src/pages/og/[...routes].ts.
This will dynamically generate images for your content.
Here’s how I set it up for my newsletter collection:

import { OGImageRoute } from "astro-og-canvas";
import { getCollection } from "astro:content";

// Fetch all newsletter entries
const newsletters = await getCollection("newsletter");

// Map newsletters to OG image configurations
const pages = {
  ...Object.fromEntries(
    newsletters.map((newsletter) => [
      newsletter.data.slug,
      {
        data: {
          title: newsletter.data.title,
          description: newsletter.data.newsletter.parts
            .map((part) => `${part.emoji.icon} ${part.title}`)
            .join(" • "),
        },
        slug: newsletter.data.slug,
      },
    ])
  ),
};

export const { getStaticPaths, GET } = OGImageRoute({
  param: "route",
  pages,
  getImageOptions: async (_, { data, slug }) => ({
    title: `Ivo's Ecotainment Newsletter: ${data.title}`,
    description: data.description,
    bgGradient: [
      [255, 221, 0], // Yellow (from logo)
      [255, 255, 255], // White
    ],
    logo: {
      path: "./public/Ivos-Ecotainment-Newsletter_Salamander.png",
      size: [1080],
    },
    border: {
      color: [83, 174, 90], // Green accent
      width: 2,
      side: "inline-start",
    },
    font: {
      title: {
        size: 42,
        weight: "Bold",
        families: ["Noto Sans", "Noto Color Emoji"],
        color: [0, 0, 0],
      },
      description: {
        size: 20,
        weight: "Normal",
        families: ["Noto Sans", "Noto Color Emoji"],
        color: [0, 0, 0],
        lineHeight: 1.4,
      },
    },
    fonts: [
      "./public/fonts/Noto_Sans/NotoSans-VariableFont_wdth,wght.ttf",
      "./public/fonts/Noto_Color_Emoji/NotoColorEmoji-Regular.ttf",
    ],
    padding: 50,
  }),
});Code language: TypeScript (typescript)


Some learned lessons:
1. Logos: SVGs won’t work here. Use PNGs instead.
2. Fonts: Add font files, if you use fonts.
3. Emojis: If you use emojis in your content, include an emoji font (like “Noto Color Emoji”) to ensure they render correctly.

Step 2: Integrate with astro-seo

I use the astro-seo component in my main Layout component to handle SEO metadata.

For the OG image I add a prop to be passed from the respective page:

const { title, description, ogImage } = Astro.props;

<SEO
  title={title}
  description={description}
  openGraph={{
    basic: {
      title,
      type: "website",
      image: ogImage || `${Astro.site}Ivos-Ecotainment-Newsletter_big.png`, // Fallback
    },
  }}
/>Code language: TypeScript (typescript)

In Your Content Pages:
Construct the OG image path and pass it to your layout:

const ogImage = `${Astro.site?.href || ""}og/${entry.data.slug}.png`;

<Layout title={entry.data.title} description={description} ogImage={ogImage}>
  {/* Post content */}
</Layout>Code language: TypeScript (typescript)

Astro Odds:

A bit odd is if your astro.config.mjs has trailingSlash: "always", you’ll need to add a trailing slash to OG image URLs in development:
http://localhost:4321/og/${entry.data.slug}.png/

In prod (no trailing slash needed) so you can just pass it as: http://localhost:4321/og/${entry.data.slug}.png

3. Testing OG Images

As recommend in the Blog post of Aidan Kinzett: Use opengraph.xyz to preview your OG images before sharing links.
Very good tool!

Here is how the OG image looks like with logo, title and description with emojis:

Happy hacking!