>

mokhtar

Dynamic OG images with Remix and Node.js

Feb 17, 2023

2β€―947 views

<tldr>
I released @m5r/og to generate OG images with Node.js like the cool kids do with @vercel/og. See instructions below.
</tldr>

Vercel recently released @vercel/og to help developers generate Open Graph (OG) images through code with a faster approach that converts HTML and CSS to SVG. It provides a much more pleasant experience* compared to previous solutions that usually involved sluggish processes like spawning a Chromium instance and taking a screenshot of the rendered HTML.
*if you're using their Edge Runtime

Squidward looking out the window meme

Me feeling left out, running my Remix apps with Node.js

Problem is, importing @vercel/og in a Remix/Node.js project results in a cryptic error and the most straightforward alternative is to deal directly with the lower level dependencies satori and possibly resvg. This is how the Remix community member Roger Stringer (@freekrai) does it in the setup he shared on the Remix Discord server.

I love how simple @vercel/og's API is and I wanted to recreate this sweet DX in my Remix apps. So I made an alternative package to @vercel/og without any of the Edge Runtime-specific stuff to make it run in Node.js. Judge for yourself the result, this was generated just now from the API:

dynamically generated og image

Feel free to open the image in a new tab and play with the query parameters.
I initially wanted to make this part of the post interactive, but I couldn't be bothered staying up all night to set up MDX πŸ˜‚ Instead, I decided to use that time to open-source the code, publish it as an npm package and write this post.

How to use?

Here is how you can use it in a Remix resource route:

npm install @m5r/og
import fs from "node:fs/promises";
import type { LoaderArgs } from "@remix-run/node";
import { ImageResponse } from "@m5r/og";

export async function loader({ request }: LoaderArgs) {
	const url = new URL(request.url);
	return new ImageResponse(
		<div
			style={{
				width: "100%",
				height: "100%",
				display: "flex",
				alignItems: "center",
				justifyContent: "center",
			}}
		>
			<h1 style={{ fontSize: 72, fontFamily: "Inter" }}>
				Hello, world πŸ‘‹
			</h1>
			<p style={{ fontSize: 24 }}>
				{url.searchParams.get("dynamic_content")}
			</p>
		</div>,
		{
			emoji: "noto",
			fonts: [
				{
					name: "Inter",
					data: await fs.readFile("./public/fonts/inter-regular.otf"),
					weight: 600,
					style: "normal",
				},
			],
		},
	);
}