Generating Open Graph images with Vercel

Published on 26/05/2024


Before kicking off, most of what I’ll cover in this post is already in Vercel’s documentation about Open Graph image generation, which is pretty awesome. That said, I’ll mostly walk through how I pulled it off on my website, which uses next-seo for managing SEO-related attributes. So, if you’re also using this stack, it might add value for you! Otherwise, this is just really documenting what I’ve just learned how to do.

Installing stuff

This whole piece is based on these two packages: @vercel/og & next-seo. Ensure you install them with your preferred package manager before following the rest.

Creating the OG image

A quick primer

Open Graph images are an element part of the Open Graph protocol, which essentially is a way to make web pages more attractive when third-party applications use their URL. Do you know when you add a link to your tweet and, after you post it, an image appears? That’s the OG image, something you can optionally add to give a brand boost to your URL.

Adding the OG API route

Here’s when we’ll start to use the @vercel/og package. It uses Vercel's infrastructure (i.e., ”Edge Functions”) to generate these images instantly. And then, the ultimate value for us is the ability to create OG images using HTML and CSS, which is a huge time-saver as opposed to managing these assets elsewhere.

For example, before giving my website this update, I created each page’s image by hand, using a combination of Figma and any hosting image site out there to generate a URL I could add to my NextSeo component. I know... that was a terribly bad process.

Alright, to start us off, let's create a file for the OG image component in this specific route: pages/api/og.jsx. Then, go ahead and copy this code snippet (adapted from Vercel’s docs):

1import { ImageResponse } from '@vercel/og';
3export const config = {
4  runtime: 'edge',
7export default function handler(request) {
8  try {
9    const { searchParams } = new URL(request.url);
11    const hasTitle = searchParams.has('title');
12    const title = hasTitle
13      ? searchParams.get('title')?.slice(0, 100)
14      : 'My default title';
16    return new ImageResponse(
17      (
18      <div
19        style={{
20          fontSize: 40,
21          color: 'black',
22          background: 'white',
23          width: '100%',
24          height: '100%',
25          padding: '50px 200px',
26          textAlign: 'center',
27          justifyContent: 'center',
28          alignItems: 'center',
29        }}
30      >
31        {title}
32      </div>
33      ),
34      {
35        width: 1200,
36        height: 630,
37      },
38    );
39  } catch (e) {
40    console.log(`${e.message}`);
41    return new Response(`Failed to generate the image`, {
42      status: 500,
43    });
44  }

You’ll see a few things here:

  • It uses an async function, so the OG image generation doesn’t compromise any other function on your page or site.
  • It uses the edge runtime configuration to leverage Vercel’s infrastructure, which is essentially just a way to retrieve information faster as it’s pulling from the data center closest to you. If you have a small website like mine, no need to worry about this at all.
  • It uses a try...catch statement to house the ImageResponse component, protecting the API in case any error occurs. Note that we’re already passing the title as a prop here, which is also already coming in with a default value in case there’s no specific title passed via the URL.
  • Finally, if you visit the route https://localhost:3000/api/og, you should see a PNG created using the exact HTML and CSS you see there!

With this code snippet, the OG image generation is basically done. You should be able to go crazy with the design and do whatever makes sense for your site. Vercel has cool examples set up so you learn how to take the design and DX (for example, using Tailwind instead of vanilla CSS) to the next level.

Hooking it up to next-seo

So, to then use the API created above on each page, we’ll hook this up to the NextSeo component, exported from the next-seo package, by creating a higher-level component called NextSeoHeader. Here’s my site’s component, for example:

1import { NextSeo } from "next-seo";
3export default function NextSeoHeader({ url, title }) {
4  return (
5    <NextSeo
6      title={`${title}-Danilo Leal`}
7      canonical={`${url}`}
8      openGraph={{
9        url: `${url}`,
10        title: `${title}-Danilo Leal`,
11        images: [
12          {
13            url: `${title}`,
14            alt: `${title} by Danilo Leal`,
15            width: 1200,
16            height: 630,
17          },
18        ],
19      }}
20    />
21  );

I’m adding a title and url props to fields that the NextSeo component exposes, making managing SEO attributes quite handy. Therefore, at the top of each page file, I add variables to avoid code repetition as they’re used by other components throughout. And then, I add them to this higher-level component like so:

1const postTitle = "Four simple accessibility improvements to apply today";
2const postUrl = "/my-world/thinking/four-simple-a11y-improvements";
4<NextSeoHeader title={postTitle} url={postUrl} />

Wrap up

By visiting the API route of your OG image, with your page’s title passed in as a parameter of the URL, you should be able to see what it looks like. Here’s mine:

A Open Graph image generated with Vercel

Overall, I know my setup is quite specific—for example, I don’t yet use any Markdown-based tool, like MDX, to write blog posts—which means this might not be super helpful, but the Vercel documentation and the whole ergonomics of using their package are pretty complete, so I hope that can help you out more, alongside with this quirky little post.