Dynamic Open Graph Images using Satori and Astro

Avatar for Yash DYash Deshpande Written on August 9, 2024 4 min read

Web Dev

When you share on any social media platform, some websites give you that neat preview right? So what I am doing today is dynamically making those preview images (also called Open Graph Images) for my blogs using Satori in Astro. I am implementing this also because I’m too lazy to make OG images from scratch in Figma everytime. Open Grapgh Example in . I was aware of Satori and how it made SVG’s from HTML and CSS and it was on my TODO list for a long time but I was procrastinating on actualy implementing until I read this article by dietcode. The article’s imlpementation used a Astro post build hook but since I am using Server Side Rending for Vercel analytics, I’ll have to modify it to run pre build because then it would be bundeled with the Vercel Adapter Build. Here is a quick TL;DR of my hacky implementation in Astro

Vercel’s Open Graph Playground is a great resource to visualize and design the image. Satori does not have the complete CSS implementation and its supposed to be written in a JSX like syntax (although experimental TailwindCSS implementation is present) so using that playground to exactly visualize how the output would be is quite helpful. Here is my implementation Open Graph Example in . Now that we have the JSX code for generating the SVG, I’ll be converting that into object that looks like transpiled JSX because I haven’t installed React or any JSX Compiler. That code looks something like this:

const JSX={
		type: "div",
		props: {
			children: "hello, world",
			style: { color: "black" },
		    },
        },

Outline of Script

So this is what the script does,though I think you’ll understand better by reading the source code. I made a js file in the root directory in which, first a function with the title of each post as parameters is made and it returns the transpiled JSX object with the title of the blog.

const svg = (title) => ({
// This is my styling In transpiled JSX syntax. This is not the complete thing, complete thing was too big, here is a small smippet of it
type: "div",
props: {
        {
		type: "p",
		props: {
			children: title,
			style: {
				marginTop: 40,
				fontSize: 52,
				display: "flex",
				padding: 10,
				alignItems: "center",
			}, },
        },
    }
});

Then a font file is loaded as a ArrayBuffer using fs. We read each file in the posts directory, parse it using grey-matter to get the frontmatter (Astro’s content collection was not working here so grey matter it is). After parsing, I’ll get the title of the post which will be passed to the above function and gets the transpiled JSX Object with the title. Now we call Satori and pass in our object along with other parameters like height, width of the image and the font ArrayBuffer. Finaly, we convert the SVG to a PNG using Resvg and write it do the public/og directory as Astro doesn’t touch this directory and passing this path as props is easier.

const outfit = fs.readFileSync("./Outfit-Bold.ttf"); // Font file ArrayBuffer is needed by Satori
const postsDirectory = "src/content/posts";
const files = fs.readdirSync(postsDirectory);

files.forEach(async (file) => {
	const filePath = `${postsDirectory}/${file}`;
	const fileContent = fs.readFileSync(filePath, "utf-8");
	const title = parseFrontmatter(fileContent).data; // just reading and parsing each post to get frontmatter of each to pass on to our above styled div

	const output = await satori(svg(title.title), {
		width: 1200,
		height: 430,
		fonts: [
			{
				name: "Outfit",
				data: outfit,
				weight: 700,
				style: "bold",
			},
		],
	});
	const resvg = new Resvg(output, {
		// converting to PNG's
		fitTo: {
			mode: "width",
			value: 1200,
		},
	});
	fs.writeFileSync(
		`public/og/${slugify(title.title, {
			lower: true,
			trim: true,
			remove: undefined,
		})}.png`,
		resvg.render().asPng()
	);
});

Now that we have our little script ready, I made the public/og directory and modified the package.json to include our script before the build.

"scripts": {
    "build": "node satori.js && astro check && astro build",
},

All that’s left is passing the Open Graph Content to the Layout file like this:

//This is Layout.astro accepting the prop
---
const { imgurl, title, description } = Astro.props;
---

<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imgurl} />
// This is the slug.astro, passing the props
<Layout
	title={entry.data.title}
	imgurl= `https://yashd.tech/og/${slugify(entry.data.title, {
			lower: true,
			trim: true,
			remove: undefined,
		})}.png`
	description={entry.data.description}
></Layout>

Improvements that can be made

Currently the script is generating the images that were previously generated once more and overiding the previous version just adding time and increasing computation. Right now I’m hosted on Vercel so build times are not my worries and my blogs right now are at a very small scale, once they reach bigger, the script will be revisited and improvised.