Harnessing the power of Next.js and `@vercel/og` to dynamically generate some pretty cool images that work great as social cards!
@vercel/og
?Okay! Here's how I did it: site/src/pages/api/img/bordered.tsx
Think you can do it better? Open a PR! I'd love to explore it with you 💕
By themselves, links are pretty boring. They're just some text that points at a page somewhere on the Internet.
With social cards (generated from OpenGraph data), links become like online business cards for our sites. They allow us to bring our links to life by attaching a title, description, and image to our links to make them beautiful (and noticeable).
The title, description, and image are specified by meta
tags in a page's
head
. This means that each page can have its own unique social card!
@vercel/og
?While each page can have a unique social card, it can be quite a burden to then create an image for each page.
That's where
@vercel/og
comes in!
@vercel/og
is a library that allows us to create an
API route that generates an
image using JSX and a subset of CSS.
And the coolest part is that since it's just another route, we can dynamically
alter the generated image by attaching search parameters to the URL
(/api/img/bordered?title=Title&description=Description
).
Let's play with an example!
I used @vercel/og
to build a bordered image endpoint. It uses the title
and
description
search params to dynamically generate the text. The emoji border
is randomized each time the image is generated.
Change the title and description fields and watch as the image and search params update automatically! New images might take a bit to generate, so please be patient! ❤️
title=Title&description=Description
Isn't that so fun‽
Now that we've experienced how fun and powerful @vercel/og
is, let's use it to
build ourselves an endpoint!
First up, we need to install it: pnpm i @vercel/og
(replace pnpm
with your
package manager of choice)
We'll use /api/img/basic
as our URL, so next we'll create
pages/api/img/basic.tsx
.
Now we'll add a default
export of a function that returns an ImageResponse
called with some JSX. We'll also configure the endpoint to use the
experimental-edge
runtime which is required for @vercel/og
.
⬇️pages/api/img/basic.tsxtsx
import {ImageResponse} from '@vercel/og'export const config = {runtime: 'experimental-edge',}export default function BasicImg() {return new ImageResponse(<div>Hello World</div>)}
Wow, that's a big image with almost nothing in it!
By default, @vercel/og
generates images at a size of 1200x630. If we want to
change that, we can supply the width
and height
options to ImageResponse
:
⬇️tsx
export default function BasicImg() {return new ImageResponse(<div>Hello World</div>, {width: 400,height: 200,})}
That's a little better but there's still not much to it.
Let's add a few improvements:
⬇️tsx
export default function BasicImg() {return new ImageResponse((<divstyle={{width: '100%',height: '100%',display: 'flex',alignItems: 'center',justifyContent: 'center',fontSize: 120,backgroundColor: 'white',border: '1px solid black',}}>Hello World</div>),{width: 400,height: 200,},)}
Neat! It's just like plain ol' CSS-in-JS. Which is great if you're a CSS-in-JS person, but not so great if you're not.
If you're not one of those people, you're in luck because there's not much to it!
Things are styled using a style
object where the keys are the same display
-
and max-width
-type attributes you're used to, except camelcased (maxWidth
).
And the values are strings unless they're a number but also numbers can be strings too if you want, because CSS is a very forgiving language 🙏
One other thing to note about the above image is that the outer container needs
to have a width
and height
of 100%
in order for flex
to center the text:
tsx
<divstyle={{width: '100%',height: '100%',display: 'flex',alignItems: 'center',justifyContent: 'center',fontSize: 120,backgroundColor: 'white',border: '1px solid black',}}>Hello World</div>
There are also a few other differences we need to be aware of! The underlying
library that @vercel/og
uses to generate the images is
@vercel/satori
.
Satori handles only a subset of the CSS spec. It includes common features, but
it's limited in many ways (like flex
and none
being the only options for
display
). The Satori README
contains a table with the supported properties.
If you plan to use @vercel/og
, be sure to look through the table to get a
better understanding of what you can and cannot do with it.
Since it's just JSX, we can create components! Let's refactor the code we have
so far as two components: Container
and Text
⬇️tsx
import type {ReactNode} from 'react'const Container = ({children}: {children: ReactNode}) => (<divstyle={{width: '100%',height: '100%',display: 'flex',alignItems: 'center',justifyContent: 'center',backgroundColor: 'white',border: '1px solid black',}}>{children}</div>)const Text = ({children}: {children: ReactNode}) => (<divstyle={{fontSize: 64,}}>{children}</div>)export default function BasicImg() {return new ImageResponse((<Container><Text>Hello World</Text></Container>),{width: 400,height: 200,},)}
Serving components as an image as an API response. Yum!
There's lots more to explore like automatically rendering different emoji sets, custom fonts, and more!
Unfortunately, I'm out of time and energy for this page at the moment. Maybe I'll add more content later!
In the meantime, be sure to follow me on Twitter, check out the official Vercel OG Image Generation documentation, and play around with the OG Image Playground!