Dynamic OG Images With Remix
I have a Remix hobby project I'm hacking on and thought it would be cool to generate dynamic open graph images for. (The preview images you see for links shared on social media.)
I had heard of the Satori package that can generate SVG's from HTML and CSS. This lead me to the @vercel/og package. I was initially pretty jealous because I just assumed it wouldn't work with Remix, and it looked pretty sweet.
- Uses JSX
- Supports Tailwind (not every css class but enough for me)
- Supports dynamic images
- Supports loading fonts
The post I came across referenced Vercel's "edge" pretty heavily and my Remix app is just running in a single docker container on a single Hetzner VPS. I decided to give it a shot and was pleasantly surprised that it worked just fine in a Remix loader. 🎉
What I'm generating
My app allows people to create lists of books. Each book can have a cover image assigned to it. I wanted the OG image to contain the title of the list, the user's username with their profile picture, and the first 6 book covers but have the covers slightly blurred to provide some click bait. Whoever sees the OG image will have to click the link to see the un-blurred books. I know. Shame on me.
Here's an example:
(Disclaimer: Ryan Holiday doesn't actually use the app 😅)
The problem
I quickly discovered that since there are so many images being incorporated, it was taking around ~4 seconds to generate the image on my 3 vCPU / 4 GB Cloud VPS. Sites like facebook and twitter would make a request to get the image, give up(there'd be no preview image in the social media post), and then I'd still see subsequent requests to fetch it again in the server logs immediately after. Multiple http requests would be generating the same image at once which would significantly impact the overall performance of the application. That wasn't going to work.
The solution
The solution was rather simple. Instead of generating the images on the fly, I now generate the image beforehand, store it(R2), and then serve that from my remix loader. Luckily, I have a pretty good event to latch onto for this. In the app, all lists are unpublished initially. The user can publish them whenever they're ready. It was in that action it made sense for me to kick off the OG image generation.
The tradeoffs
The major tradeoff with this approach and how I have my app setup is that the image can easily be outdated. For example, if they delete a book from the list or reorder them after they've already published it. Not a huge problem but something to be aware of. This is for social media images, after all. I could figure out a way to prevent this but the payoff isn't there. I'd rather focus on more interesting and valuable problems to solve right now.
Hiccups along the way
I noticed the tailwind blur classes were not supported but that was easily resolved by adding style
tags to the images:
style={{ filter: "blur(5px)"}}
Fonts were also a little tricky. I am just trying to use Arial and Georgia here. I ended up having to download the ttf files from my mac's Font Book by right clicking and selecting Show in Finder. Then copy/pasting the Arial.ttf
into my remix app's public
folder.
Once it was there, I can fetch
it in my loader and incorporate it into the ImageResponse
.
I'm pretty sure there's an easier, more efficient way to do this but it's what I came up with that worked.
Here's the final result:
Categories: Node