Balázs Barta

Design Technologist

Back

Creating dynamic OG images in Astro

Balázs Barta ∙ Published on February 24, 2025
Creating dynamic OG images in Astro

Creating Open Graph images manually is a pain. Opening Figma, updating templates, exporting, uploading - and repeating this for every single post. While tools like Figma make the process easier, it’s still a workflow from the past, not 2025. Let’s fix this tedious process once and for all using Astro and astro-og-canvas, bringing OG image generation into the modern age.

The problem with manual OG images

During my time at a marketing agency, creating OG images was a daily ritual. Every client had their own template - first in Photoshop, later in Figma - carefully crafted to match their brand guidelines. While having unique templates streamlined the process, it was still a time sink. For each piece of content, someone had to open the template, update the copy, maybe swap the background image, export, and upload.

What started as a manageable task for a few clients quickly became overwhelming as our client base grew. Even with well-organized templates, we were spending hours each week just on OG images. The real headache came when clients wanted to update their published content - each change meant repeating the entire process. And let’s not even mention the occasional “emergency” requests where rushing led to wrong dimensions or forgotten brand guidelines.

Enter astro-og-canvas

astro-og-canvas solves this problem elegantly. It generates Open Graph images programmatically, ensuring consistent branding while adapting to your content automatically. Let’s see how to implement it.

First, install the package:

bash
yarn add astro-og-canvas

or with npm:

bash
npm i astro-og-canvas

Now, create a new endpoint that will generate your OG images. Here’s a basic setup:

TypeScript
// src/pages/open-graph/[...route].ts
import { OGImageRoute } from "astro-og-canvas";
import { getCollection } from "astro:content";

export const { getStaticPaths, GET } = OGImageRoute({
  param: "route",

  // Generate OG images for all blog posts
  // Replace "blog" with your collection name
  pages: async () => {
    const posts = await getCollection("blog");

    return posts.reduce((acc, post) => ({
      ...acc,
      [post.slug]: {
        // replace with your collection's title and description
        title: post.data.title || "Default title",
        description: post.data.description || "Default description",
      },
    }));
  },

  // Basic template for our OG images
  getImageOptions: (path, page) => ({
    title: page.title,
    description: page.description,
    logo: {
      path: "./src/assets/logo.png",
    },
  }),
});

Making it your own

The real power of astro-og-canvas comes from its customization options. You can control every aspect of your OG images, from fonts to layouts. Here’s how I style mine:

TypeScript
// src/pages/open-graph/[...route].ts
import { getCollection } from "astro:content";
import { OGImageRoute } from "astro-og-canvas";

export const prerender = true;

const blogs = await getCollection("blog");
const pages = Object.fromEntries(
  blogs.map(({ id, slug, data }) => [id, { data, slug }]),
);


export const { getStaticPaths, GET } = OGImageRoute({
  param: "route",
  pages,
  getImageOptions: (path: string, { data }: (typeof pages)[string]) => ({
    title: data.title,
    description: data.description ? data.description : "",
    // You can use custom fonts if you want
    fonts: [
      "./public/fonts/Inter-Regular.ttf",
      "./public/fonts/Inter-Bold.ttf",
    ],
    font: {
      title: {
        size: 44,
        lineHeight: 1.3,
        families: ["Inter"],
        weight: "Bold",
      },
      description: {
        size: 30,
        lineHeight: 1.6,
        color: [115, 115, 115],
        families: ["Inter"],
        weight: "Normal",
      },
    },
    bgImage: {
      path: "./src/assets/og-bg.png",
      fit: "contain",
    },
    padding: 80,
    logo: {
      path: "./src/assets/logo.png",
      size: [50, 60],
    },
    quality: 100,
  }),
});

It’ll generate an OG image which looks like this:

Implementing in your blog

To use your dynamic OG images, add these meta tags to your blog post template:

Astro
---
// src/pages/blog/[slug].astro

// In this case you can test it in development mode
const baseUrl = import.meta.env.DEV
  ? "http://localhost:4321"
  : new URL(Astro.request.url).origin;

const { slug, title, description } = post.data;

const ogImage = new URL(`/open-graph/${slug}.png`, baseUrl).toString();
---

<head>
  <meta property="og:image" content={ogImage} />
  <meta property="twitter:image" content={ogImage} />
</head>

Common pitfalls to avoid

After implementing this on my own blog, I’ve learned a few important lessons:

  • Font paths matter: Ensure your fonts are accessible during build time. I store mine in ⁠public/fonts.
  • Test across platforms: What looks perfect on Twitter might clip on LinkedIn. Always test on multiple platforms.
  • Watch your contrast: Dark text on dark backgrounds is a common mistake. Test your templates with various content lengths.

Looking ahead

Dynamic OG images are just the beginning. You could extend this system to generate:

  • Different templates for different content types
  • Seasonal variations of your OG images
  • A/B tests for social engagement

The power of automation

The switch from manual to automated OG images has saved me countless hours. More importantly, it’s eliminated a major source of inconsistency in my content workflow. Every post now gets a perfectly branded, properly sized OG image without opening Figma. Next time you find yourself manually creating social preview images, remember: automation isn’t just about saving time - it’s about maintaining consistency and reducing human error. Your content deserves that level of professionalism.