Open-graph image generation with Astro

6 Dec 2023

In this guide, I’ll demonstrate how to automate the addition of generated open-graph images to your Astro websites, automating yet another tedious task.

Open-graph Images

Open-graph image example

Here’s a quick reminder about open-graph images. These are the images displayed when you share your link. They’re easy to add—just insert the following meta tag into your HTML’s head. However, while simple to add, they can be time-consuming to create and manage.

<meta property="og:image" content="path-to-my-image.png" />


I recently shared my website template, Launch, which I use for all my websites including JXD, DocLabs, and Alphabee. It automates many of the tedious, yet crucial tasks related to managing a website, allowing me to spend more time developing the products. Automating the creation of images across all websites significantly saves me time.


Install the @vercel/og package. This library is designed to convert React code into PNG images. It is built on Satori, a library that converts HTML and CSS into SVGs.

npm install @vercel/og

Create a new static resource endpoint for your images. In Launch, this is located at src/pages/blog/og/[...slug].png.ts. However, you can create it elsewhere, provided that it:

  1. Resides in the pages directory
  2. Includes the ...slug path parameter
  3. Has the .png.ts extension.

Add the following content:

export const prerender = true;

import type { APIRoute } from "astro";
import { ImageResponse } from "@vercel/og";
import { createElement as el } from "react";
import { readFileSync } from "fs";

// Load your posts however you like
import { fetchPosts } from "@/lib/blog";

type Props = {
  title: string;

export const GET: APIRoute<Props> = ({ props }) => {
  const interBold = readFileSync("./src/fonts/Inter-Bold.ttf");

  return new ImageResponse(
        tw: "w-full h-full flex justify-center items-center bg-slate-50 text-5xl uppercase text-slate-800 font-bold",
        style: {
          fontFamily: "'Inter'",
      width: 1200,
      height: 630,
      fonts: [
          name: "Inter",
          data: interBold,
          style: "normal",
          weight: 700,

export async function getStaticPaths() {
  const posts = await fetchPosts();
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { title: post.data.title },

Let’s take a deeper look at what is happening here.

First, we use export const prerender = true, which instructs Astro to build these images at build time. This is possible because the images don’t change, and we deploy a new version each time a new post is published. However, if your content comes from a different source like a CMS, consider using an edge function and caching the results.

We’re loading a font for use in our images that we previously downloaded and stored in our source code.

An ImageResponse is returned with our image content. This may appear unusual if you’re accustomed to working with JSX. However, since Astro doesn’t support .jsx or .tsx endpoints, we’re using raw React with the createElement function. In our div, we use the tw attribute to specify which Tailwind classes to apply. While I’m a huge fan of Tailwind, you could opt for inline styles if that’s your preference.

Finally, we’re defining a getStaticPaths function to load our blog posts and pass the slug and title values.

We have now generated open graph images for each blog post. An example of this can be seen on Launch by visiting https://launch.jxd.dev/blog/og/hello-world.png.

Don’t forget to add the meta tag to your blog post content so your images are included.

<meta property="og:image" content={`/blog/og/${post.slug}.png`} />


That concludes the process of adding generated open-graph images to an Astro website. If you’re planning to launch a new website from scratch, consider checking out Launch. It offers more than just open-graph images. It includes a functional blog, RSS feeds, a sitemap, robots.txt, analytics, and more.

Until next time ✌️