<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
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:
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",
},
],
},
);
}