Generating OpenGraph images with Astro
Providing OpenGraph images for websites enables other tools to render rich previews when your content is shared.
I used to manually add an image for each of my posts and embedded it using the astro-seo integrations. This approach works well in general, but I wanted to have a bit more branding with these images. Of course, I could have put some more effort into creating these images, using a Photoshop template and manually adding titles on top but, well, that would be pretty boring, right?
With Astro, it’s possible to create Static File Endpoints, which I use to create my RSS feed for example. I tried to use this to create an image endpoint instead, utilizing the @vercel/og package to generate an image response for a given JSX construct.
I’m using Content Collections for my gallery and thoughts areas. So far, my pages structure looked like this:
/thoughts/index.astro
/thoughts/[...slug].astro
I moved the detail page to a subfolder and renamed it to index.astro
, so existing URLs for post will remain working. Next to this, I created a new file for the image. The new structure now looks like this:
/thoughts/index.astro
/thoughts/[slug]/index.astro
/thoughts/[slug]/og.png.ts
To get these image generated for every post in the thoughts
collection, I’m utilizing the getStaticPaths
function, Astro provides:
import { type CollectionEntry, getCollection } from "astro:content";
export async function getStaticPaths() {
const thoughts = await getCollection("thoughts");
return thoughts.map((thought) => ({
params: { slug: thought.slug },
props: { thought },
}));
}
Now comes the fun part - generating an image. We add a GET
handler for this endpoint, receiving the individual post data via props. Inside, we use the @vercel/og
package, to create an ImageResponse
. Sadly Astro does not support JSX inside these endpoints, so I used the object syntax for React content:
export async function GET({ props }: Props) {
const { thought } = props;
const html = {
type: "div",
props: {
children: "Hello World",
tw: "w-full h-full flex items-center justify-center bg-white",
}
};
return new ImageResponse(html, {
width: 1200,
height: 630
});
}
Using the tw
prop, it’s even possible to utilize Tailwind CSS for styling the output. This is the result:
Here is an extended example, using custom fonts and images:
import fs from "fs";
import path from "path";
import logo from "./logo.png";
const loadImage = (src: string): Buffer | undefined => {
return fs.readFileSync(
process.env.NODE_ENV === "development"
? path.resolve(src.replace(/\?.*/, "").replace("/@fs", ""))
: path.resolve(src.replace("/", "dist/")),
);
};
export async function GET({ props }: Props) {
const { thought } = props;
const FigtreeRegular = fs.readFileSync(
path.resolve("./public/fonts/Figtree-Regular.ttf"),
);
const logoImage = loadImage(logo?.src);
const html = {
type: "div",
props: {
tw: "w-full h-full flex flex-col items-center justify-center bg-white",
children: [
{
type: "div",
props: {
children: thought.data.title,
style: {
fontFamily: "Figtree Regular"
}
}
},
{
type: "img",
props: {
src: logoImage.buffer
}
}
]
}
};
return new ImageResponse(html, {
width: 1200,
height: 630,
fonts: [{
name: "Figtree Regular",
data: FigtreeRegular.buffer,
style: "normal",
}]
});
}
From there, it’s only a matter of creativity to come up with a nice design. Finally, add this new image path as OG image.