Tutorial: Using Vercel's OG Image Library to Create Randomized OG Images
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.
- The
config
object at the top is important because the images are generated using Vercel Edge Functions and thisconfig
object is what lets us use them. 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.- 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!
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 😁)
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.
You might also want to check out:
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!