Prismic Meetup

Growth Masterclass: Advanced Competitive Analysis

Watch ↗

Tutorial: Using Vercel's OG Image Library to Create Randomized OG Images

Written by Coner Murphy in A speed gauge for optimization Optimization on January 01, 1970
Next.js

OG images on their own aren’t a new concept; they’ve been around for years and have become a staple for sharing content on social media and online in general. But, in the past, you’ve either had to generate your own OG images manually, which can be a laborious process, or you’ve used some form of automation for creating them, like the service Cloudinary offers.

The service Cloudinary offers is good but it does have limitations, most notably you control the image created by changing URL parameters and that’s it. This is fine, but it doesn’t give you the same flexibility and customizability that using HTML and CSS directly offers. Luckily, this is the gap that the @vercel/og package from Vercel fills.

In this tutorial, we’re going to look at how we can use this package with Vercel’s edge functions and Next.js API routes to create dynamic OG images using HTML, CSS, and JavaScript directly! And we'll even see how easy it is to incorporate Tailwind CSS into our layouts.

After we’ve finished the tutorial, we’ll have created an API route that we can hit on-demand to create and return unique OG images for each blog post.

Implementing Vercel OG images in Next.js

Project setup

To get started with generating dynamic OG images in Next.js, create a new Next.js project using the following command:

 
npx create-next-app@latest --ts PROJECT_NAME

Be sure to replace PROJECT_NAME with the name of your project. After answering any prompts and waiting for the process to finish, you’ll have a brand new Next.js project ready to go.

Note: I’m using TypeScript in this tutorial but it isn’t required; you can use standard JavaScript if you prefer. You’ll just need to convert the code in this tutorial to standard JS if you prefer.

After we have our new Next.js project, we need to install the package that is going to make all the magic happen, that is @vercel/og. To do this, cd into your new project (if you haven’t already), and then run the following command to install the package:

 
npm i @vercel/og 

Creating a basic OG image

Now, the time has come to make our API route so that whenever we hit it, we get an OG image back. To do this, create a new file under the /pages/api directory called og.tsx.

With your new API route created, paste the below code into the file, and let’s create our first OG image!

 
import { ImageResponse } from '@vercel/og';

export const config = {
  runtime: 'experimental-edge',
};

export default function () {
  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'flex-end',
          justifyContent: 'flex-start',
          position: 'relative',
        }}
      >
        <div
          style={{
            backgroundColor: '#e5e5f7',
            opacity: 0.6,
            backgroundImage:
              'linear-gradient(135deg, #444cf7 25%, transparent 25%), linear-gradient(225deg, #444cf7 25%, transparent 25%), linear-gradient(45deg, #444cf7 25%, transparent 25%), linear-gradient(315deg, #444cf7 25%, #e5e5f7 25%)',
            backgroundPosition: '10px 0, 10px 0, 0 0, 0 0',
            backgroundSize: '10px 10px',
            backgroundRepeat: 'repeat',
            width: '100%',
            height: '100%',
            position: 'absolute',
          }}
        />
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'flex-start',
            justifyContent: 'center',
            marginLeft: '20px',
          }}
        >
          <h1
            style={{
              fontSize: 72,
              color: 'black',
              marginBottom: 0,
							backgroundColor: 'white',
            }}
          >
            Some title
          </h1>
          <p
            style={{
              marginTop: 0,
              fontSize: 32,
              color: 'black',
              fontWeight: 700,
							backgroundColor: 'white',
            }}
          >
            Some description
          </p>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  );
}

For now, we have a static title and description, but don’t worry — we’ll fix that soon. A couple of things to call out from the code above.

  1. The config object at the top is important because the images are generated using Vercel Edge Functions and this config object is what lets us use them.
  2. ImageResponse is the constructor that the @vercel/og package exposes and that makes all of this possible. You can learn more about the different options you can pass to it here.
  3. For the background properties, I’m using generated CSS styles from this website. You can also generate your own and switch out the ones I’ve pre-included for you. You’ll see a few more of these in the coming steps as well.

With this configured, if you start up your development server using npm run dev, and head over to http://localhost:3000/api/og in your browser, you should see our first OG image!

A screenshot of a basic OG image created with Vercel's library. It has a dotted background with the text, "Some title - Some description."

A cartoon graphics space scene showing an astronaut exploring space as rockets and planets float around.

Stay on Top of New Tools, Frameworks, and More

Research shows that we learn better by doing. Dive into a monthly tutorial with the Optimized Dev Newsletter that helps you decide which new web dev tools are worth adding to your stack.

PageNotFoundError

When developing locally on the API route and refreshing the page to see your changes, you might encounter an error along the lines of PageNotFoundError: Cannot find module for page: XX; a restart of the local development server fixes it and you can continue on as normal.

This is likely due to the edge functions and the @vercel/og package still being relatively experimental at the time of writing so their stability should improve over time!

Randomizing our OG image background

It’s a bit boring having the same pattern and CSS background for each OG image generated, so let’s mix it up a bit and pick a random background from a selection of three every time a new image is generated. To add this functionality, we need to make some minor adjustments to the code.

First, at the top of the file, we need to add a backgrounds array with our selection of generated backgrounds between the config object we looked at before and our exported function. We also need to set up a variable we can use to randomize the background chosen from our array.

 
// ...Vercel Edge functions config object

const backgrounds = [
  {
    backgroundColor: '#e5e5f7',
    opacity: 0.6,
    backgroundImage:
      'linear-gradient(135deg, #444cf7 25%, transparent 25%), linear-gradient(225deg, #444cf7 25%, transparent 25%), linear-gradient(45deg, #444cf7 25%, transparent 25%), linear-gradient(315deg, #444cf7 25%, #e5e5f7 25%)',
    backgroundPosition: '10px 0, 10px 0, 0 0, 0 0',
    backgroundSize: '10px 10px',
    backgroundRepeat: 'repeat',
  },
  {
    backgroundColor: '#e5e5f7',
    opacity: 0.6,
    backgroundImage:
      'linear-gradient(30deg, #444cf7 12%, transparent 12.5%, transparent 87%, #444cf7 87.5%, #444cf7), linear-gradient(150deg, #444cf7 12%, transparent 12.5%, transparent 87%, #444cf7 87.5%, #444cf7), linear-gradient(30deg, #444cf7 12%, transparent 12.5%, transparent 87%, #444cf7 87.5%, #444cf7), linear-gradient(150deg, #444cf7 12%, transparent 12.5%, transparent 87%, #444cf7 87.5%, #444cf7), linear-gradient(60deg, #444cf777 25%, transparent 25.5%, transparent 75%, #444cf777 75%, #444cf777), linear-gradient(60deg, #444cf777 25%, transparent 25.5%, transparent 75%, #444cf777 75%, #444cf777)',
    backgroundPosition: '20px 35px',
    backgroundSize: '0 0, 0 0, 10px 18px, 10px 18px, 0 0, 10px 18px',
  },
  {
    backgroundColor: '#e5e5f7',
    opacity: 0.6,
    backgroundImage: 'linear-gradient(45deg, #444cf7 50%, #e5e5f7 50%)',
    backgroundSize: '24px 24px',
  },
];

const randomBackgroundNumber = Math.floor(Math.random() * backgrounds.length);

// ...exported function

Then, we need to replace the background properties on the second div element with ...backgrounds[randomBackgroundNumber]; this allows us to spread in the selected styles from the backgrounds array.

 
<div
  style={{
    ...backgrounds[randomBackgroundNumber],
    opacity: 0.6,
    width: '100%',
    height: '100%',
    position: 'absolute',
  }}
/>

With these small changes completed, you should be able to refresh (you may need to do a hard refresh: CTRL + SHIFT + R) your API route in your browser and receive a new OG image with a different background when you refresh.

Dynamic title and description

Having random backgrounds is cool and all, but the OG image isn’t much help to us if we’re stuck with the same generic text each time. So, now we’re going to edit our exported function to allow us to take URL parameters and then put those parameters into the image itself. We’re going to use two URL parameters for this, these are title and description.

To get started, at the top of the file, we need to import NextRequest using import { NextRequest } from 'next/server';. Then inside our function, we need to add some logic to take the URL parameters out of the request and display them.

 
// ...backgrounds array and config object as before

export default function (req: NextRequest) {
  try {
    // 1: get the searchParams from the request URL
    const { searchParams } = new URL(req.url)

    // 2: Check if title or description are in the params
    const hasTitle = searchParams.has('title')
    const hasDescription = searchParams.has('description')

    // 3: If so, take the passed value. If not, assign a default
    const title = hasTitle
      ? searchParams.get('title')?.slice(0, 100)
      : 'Some title'

    const description = hasDescription
      ? searchParams.get('description')?.slice(0, 100)
      : 'Some description'

    return new ImageResponse(
      (
        <div
          style={{
            width: '100%',
            height: '100%',
            display: 'flex',
            textAlign: 'center',
            alignItems: 'flex-end',
            justifyContent: 'flex-start',
            position: 'relative',
          }}
        >
          <div
            style={{
              ...backgrounds[randomBackgroundNumber],
              opacity: 0.6,
              width: '100%',
              height: '100%',
              position: 'absolute',
            }}
          />
          <div
            style={{
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'flex-start',
              justifyContent: 'center',
              marginLeft: '20px',
            }}
          >
            <h1
              style={{
                fontSize: 72,
                color: 'black',
                marginBottom: 0,
                backgroundColor: 'white',
              }}
            >
              Some title
            </h1>
            <p
              style={{
                marginTop: 0,
                fontSize: 32,
                color: 'black',
                fontWeight: 700,
                backgroundColor: 'white',
              }}
            >
              Some description
            </p>
          </div>
        </div>
      ),
      {
        width: 1200,
        height: 600,
      }
    )
  } catch (e) {
    return new Response(`Failed to generate the image`, {
      status: 500,
    })
  }
}

The final part of the code we need to adjust is the ImageResponse elements that are displayed on the screen. We need to change Some title to {title} and then Some description to {description}.

 
<h1
  style={{
    fontSize: 72,
    color: 'black',
    marginBottom: 0,
    backgroundColor: 'white',
  }}
>
  {title}
</h1>
<p
  style={{
    marginTop: 0,
    fontSize: 32,
    color: 'black',
    fontWeight: 700,
    backgroundColor: 'white',
  }}
>
  {description}
</p>

With all these changes completed, we’re now ready to give our dynamic OG image a test run. To do this, head over to http://localhost:3000/api/og?title=test%20title&description=hello%20world, and then you should see something like the image below. (Your background may be different though 😁)

A screenshot of an OG image similar to the one earlier, but now populated with dynamic content from our query params, "Test title - hello world."

Now, if any of the two parameters aren’t included in the URL, then they will default back to the values we provided, which would look something like the OG image we started with.

And, that’s it! We now have a dynamic OG image that uses a random background from a pre-defined selection with dynamic inputs from the URL. But, we can go one step further by switching out our styling from vanilla CSS to TailwindCSS! Let’s do that next.

Adding TailwindCSS styles

You don’t need to do anything to enable TailwindCSS support as it comes configured out of the box with the @vercel/og package. You don’t even need to have TailwindCSS set up and configured in your project! 🤯

If you want to use TailwindCSS styles with your OG image, all you need to do is use the tw property name on your elements like so.

 
// ...rest of function, and backgrounds/config setup

return new ImageResponse(
  (
    <div tw="w-full h-full flex text-center items-end justify-start relative">
      <div
        tw="w-full h-full absolute"
        style={{
          ...backgrounds[randomBackgroundNumber],
        }}
      />
      <div tw="flex flex-col items-start justify-center ml-5">
        <h1 tw="text-7xl text-black mb-0 bg-white">{title}</h1>
        <p tw="text-3xl text-black mt-0 font-bold bg-white">{description}</p>
      </div>
    </div>
  ),
  {
    width: 1200,
    height: 600,
  }
);

// ...rest of file

With this quick change, we have the same OG images as before being generated, but now they’re using TailwindCSS instead (apart from the random backgrounds, we still need vanilla CSS for those).

Now, with our dynamic OG images complete and ready to use, all we need to do is add them to our pages, so they’re ready to be shared with the world. For each page you want to give a custom OG image to, add the URL of the OG image API route as the value of the og:image meta tag in your page head. For example, for the API route used in this post, that would look like this:

 
<head>
  <meta property="og:image" content="http://localhost:3000/api/og?title=test" />
</head>

TailwindCSS & eslint

If you use eslint on your project like I do and have the react/no-unknown-property rule enabled, then the tw property in the TailwindCSS section will flag as an error because it’s not a standard property. To get around this, add an ignore object to the "react/no-unknown-property" rule in your eslint configuration.

Closing Thoughts

It’s still early days for this new offering from Vercel, but it has a lot of potential and I’m excited to see where they take this level of OG image automation in the future and how others apply it in their projects! How will you use it?

As a quick recap, in this tutorial, we’ve taken a fresh Next.js project and added the ability to create dynamic OG images using the @vercel/og package. These images have rotating backgrounds and dynamic values controlled by URL parameters. We then finished the tutorial by converting the styling to use TailwindCSS.

But, this isn’t all, you can do more with this new offering from Vercel such as Secure URLs and custom fonts. If you’re curious about what you can do, you can see all of the examples they offer in their documentation here.

And, if you’re up for a challenge, then take this tutorial project and expand it to include a blog with a few posts with each post generating its own OG image using the API route we just created. Then, deploy the project to Vercel and see if it all works by sharing one of the posts on social media!

A portrait photo of Coner Murphy in a plaid shirt.

Coner Murphy

Web Developer, technical writer, and tech entrepreneur sharing my journey to financial freedom. Building PhyType and SaaS products in public.

More posts