Dynamic OG images with Remix and Node.js

Feb 17, 2023

2β€―604 views

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

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(
                    width: "100%",
                    height: "100%",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                <h1 style={{ fontSize: 72, fontFamily: "Inter" }}>
                    Hello, world πŸ™
                <p style={{ fontSize: 24 }}>
            emoji: "noto",
            fonts: [
                    name: "Inter",
                    data: await fs.readFile("./public/fonts/inter-regular.otf"),
                    weight: 600,
                    style: "normal",