HomePosts

Open Graph Image Generation in Next.js

Is storing open graph images now a thing of the past?

February 2, 2023

I recently made the decision to switch my personal website from React to Next.js, driven in part by my upcoming role at Vercel and my desire to become more familiar with their framework.

As I delved into the Vercel docs, I came across the concept of Open Graph (OG) meta tags — in particular OG images. For those unfamiliar, an OG image is an image that accompanies a link when posted on social media.

They can have a big impact on increasing clicks and engagement — in fact, posts on LinkedIn that contain images tend to receive, on average, 98% more comments. Likewise, tweets that include visual content have been found to be three times more likely to generate engagement compared to those without.

To make my blog posts more appealing, I wanted to make sure they all had accompanying OG images — who knows, maybe that's why you're reading this!

Enter Next.js

Making OG images for a website used to be hard work that needed to be done by hand. It could take a lot of time to create and store the images, and the images might not look consistent across each post.

Upon discovering that Next.js supports OG image generation, I was stunned. That's right, each time you create a new blog post for example, the OG images will be generated automatically!

This is a game-changer for three reasons:

  1. No more manual image creation and storage headaches
  2. Consistent quality and style across all OG images and social media posts
  3. Optimal image display and appearance for each platform.

So how does it work?

OG image generation is made possible with the help of the @vercel/og library and Vercel Edge Functions. For my site, whenever I create a new blog post, an OG image is generated automatically just like the one below. OG Image Generation

Let's see how we can create an OG image like mine in Next.js. It's important to keep in mind that this guide assumes a basic understanding of working with Next.js.

...

1. Install @vercel/og

To get started, run one of the following commands to install @vercel/og in your Next.js project:

1npm i @vercel/og 2 3# or 4 5pnpm add @vercel/og 6 7# or 8 9yarn add @vercel/og 10

2. Setting up the API Endpoint

The next step is to create an API endpoint named og.tsx or og.jsx in the pages/api folder. In this endpoint we will define the appearance of our OG image.

Let's start with something simple and have the endpoint generate a Hello World image.

1import { ImageResponse } from '@vercel/og'; 2 3export const config = { 4 runtime: 'edge', 5}; 6 7export default function handler() { 8 return new ImageResponse( 9 ( 10 <div 11 style={{ 12 fontSize: 128, 13 background: 'white', 14 width: '100%', 15 height: '100%', 16 display: 'flex', 17 textAlign: 'center', 18 alignItems: 'center', 19 justifyContent: 'center', 20 padding: 40, 21 }} 22 > 23 Hello world! 24 </div> 25 ), 26 { 27 width: 1200, 28 height: 630, 29 }, 30 ); 31} 32

A couple of things worth mentioning in the code above:

  1. The image size of 1200 by 630 pixels follows Facebook's recommended size for OG images.
  2. The endpoint is set as an edge function, as explained in Vercel's docs. Edge Functions offer better performance and cost-effectiveness compared to Serverless Functions.
  3. The image is styled using React's inline style, but you can use Tailwind instead as follows:
1<div tw="p-10 w-full h-full flex items-center justify-center text-5xl text-center bg-white"> 2 Hello world! 3</div> 4

It's now time to check out your OG image in action! Navigate to http://localhost:3000/api/og on your local dev environment, and you should see the following image: static-og-image

To complete the setup, in your pages/index.tsx file replace the code with the following. Now, whenever you share this page as a link on social media, the image you just created will also appear as a preview.

1import Head from "next/head"; 2 3export default function Home() { 4 return ( 5 <> 6 <Head> 7 <title>Hello world</title> 8 <meta 9 property="og:image" 10 content={`<YOUR_APP_URL>/api/og`} 11 /> 12 </Head> 13 <h1>Hello world!</h1> 14 </> 15 ) 16

3. Customize Your Text

In some cases you will want to show custom text in your OG image. An example could be for blog posts (like the one you're reading!).

To achieve this, we'll need to pass title as a query parameter to our endpoint. We'll also add a try...catch in case our edge function throws an error when generating the image:

1export default async function handler(req: NextRequest) { 2 try { 3 const { searchParams } = new URL(req.url); 4 const title = searchParams.get("title") || "My default title"; 5 6 return new ImageResponse( 7 ( 8 <div tw="p-6 h-full w-full flex justify-center items-center"> 9 <div tw="p-10 bg-zinc-900 h-full w-full flex flex-col"> 10 <div tw="mt-16 flex text-6xl leading-normal text-gray-200">{title}</div> 11 </div> 12 </div> 13 ), 14 { 15 width: 1200, 16 height: 630, 17 } 18 ); 19 } catch (e: any) { 20 console.log(e.message); 21 return new Response(`Failed to generate the image`, { 22 status: 500, 23 }); 24 } 25} 26

As for the OG meta tags, you will probably want to move it from your home page to a dynamic page such as for a blog post. Once you've done this, you can edit the <Head> so that you can encode the title and pass it to the endpoint URL like this:

1<Head> 2 <title>{postTitle}</title> 3 <meta 4 property="og:image" 5 content={`<YOUR_APP_URL>/api/og?title=${encodeURI(postTitle)}`} 6 /> 7</Head> 8

When you visit http://localhost:3000/api/og?title=My%20OG%20Image%20And%20Some%20Custom%20Text locally, you should see the following image with the desired text: og-image-custom-text

You may have noticed that my OG image also includes some extra details such as a description and a date for when the post was published. I'm going to amend the endpoint to more closely match the style and info in my first OG image above.

1export default async function handler(req: NextRequest) { 2 try { 3 const { searchParams } = new URL(req.url); 4 const title = searchParams.get("title") || "My default title"; 5 const description = 6 searchParams.get("description") || "My default description"; 7 const publishDate = 8 searchParams.get("publishDate") || new Date().toLocaleDateString(); 9 10 return new ImageResponse( 11 ( 12 <div tw="p-6 h-full w-full flex justify-center items-center"> 13 <div tw="p-10 bg-zinc-900 h-full w-full flex flex-col"> 14 <div tw="mt-16 flex text-6xl leading-normal text-gray-200">{title}</div> 15 <div tw="mt-5 flex text-3xl text-gray-300">{description}</div> 16 <div tw="mt-5 flex items-center text-xl text-gray-300"> 17 <div>Your website</div> 18 <div tw="-mt-2 ml-3">.</div> 19 <div tw="ml-3">{publishDate}</div> 20 </div> 21 </div> 22 </div> 23 ), 24 { 25 width: 1200, 26 height: 630, 27 } 28 ); 29 } catch (e: any) { 30 console.log(e.message); 31 return new Response(`Failed to generate the image`, { 32 status: 500, 33 }); 34 } 35} 36

As for the OG meta tags, we can update the code to:

1<head> 2 <title>{postTitle}</title> 3 <meta 4 property="og:image" 5 content={`<YOUR_APP_URL>/api/og?title=${encodeURIComponent(title)}&publishDate=${encodeURIComponent(createdAt)}&description=${encodeURIComponent(description)}`} 6 /> 7</head> 8

These changes should produce an OG image that includes a custom title, description, and publish date. Try visiting http://localhost:3000/api/og?title=My%20OG%20Image%20And%20Some%20Custom%20Text&description=My%20custom%20description&publishDate=February%203%2C%202023 locally - you should see an image like the one below. dynamic-text-og-image

4. Custom fonts

In the OG image shown above, I am using the custom font JosefinSans-Regular. In my project, I've stored the font file in a folder named assets and I've updated the endpoint as follows:

1// Make sure the font exists in the specified path: 2const font = fetch( 3 new URL("../../assets/JosefinSans-Regular.ttf", import.meta.url) 4).then((res) => res.arrayBuffer()); 5 6export default async function handler(req: NextRequest) { 7 const fontData = await font; 8 try { 9 const { searchParams } = new URL(req.url); 10 const title = searchParams.get("title") || "My default title"; 11 const description = 12 searchParams.get("description") || "My default description"; 13 const publishDate = 14 searchParams.get("publishDate") || new Date().toLocaleDateString(); 15 16 return new ImageResponse( 17 ( 18 <div tw="p-6 h-full w-full flex justify-center items-center"> 19 <div 20 tw="p-10 bg-zinc-900 h-full w-full flex flex-col" 21 style={{ fontFamily: "'JosefinSans-Regular'" }} 22 > 23 <div tw="mt-16 flex text-6xl leading-normal text-gray-200">{title}</div> 24 <div tw="mt-5 flex text-3xl text-gray-300">{description}</div> 25 <div tw="mt-5 flex items-center text-xl text-gray-300"> 26 <div>Your website</div> 27 <div tw="-mt-2 ml-3">.</div> 28 <div tw="ml-3">{publishDate}</div> 29 </div> 30 </div> 31 </div> 32 ), 33 { 34 width: 1200, 35 height: 630, 36 fonts: [ 37 { 38 name: "JosefinSans-Regular", 39 data: fontData, 40 style: "normal", 41 }, 42 ], 43 } 44 ); 45 } catch (e: any) { 46 console.log(e.message); 47 return new Response(`Failed to generate the image`, { 48 status: 500, 49 }); 50 } 51} 52

Now, the OG image's font should match the one in your assets folder: og-image-custom-font

5. Incorporating an image

Sometimes, you may also want to include an external image as part of your OG image. For this example, I used my profile picture. To include an image in your OG image, add the following code just before the div element containing the title:

1<div tw="p-6 h-full w-full flex justify-center items-center"> 2 <div 3 tw="p-10 bg-zinc-900 h-full w-full flex flex-col" 4 style={{ fontFamily: "'JosefinSans-Regular'" }} 5 > 6 // If the picture is stored in your public folder, you can simply 7 // use <YOUR_APP_URL>/profile-pic.png for the src 8 <div tw="mt-10 mb-3 flex items-center"> 9 <img 10 width="84" 11 height="84" 12 src={`<LINK_TO_PROFILE_PIC>`} 13 tw="border-2 border-white rounded-full" 14 /> 15 </div> 16 <div tw="mt-16 flex text-6xl leading-normal text-gray-200">{title}</div> 17 <div tw="mt-5 flex text-3xl text-gray-300">{description}</div> 18 <div tw="mt-5 flex items-center text-xl text-gray-300"> 19 <div>Your website</div> 20 <div tw="-mt-2 ml-3">.</div> 21 <div tw="ml-3">{publishDate}</div> 22 </div> 23 </div> 24</div> 25

With these changes, your OG image should now look like this: og-image-profile-pic

6. Finishing touches

You may also recall that my OG image also has a nice gradient border that wraps around it. I'm still new to Tailwind, so if anyone knows how to do this in Tailwind please let me know!

To give your OG image a fancy border, modify the jsx code in your endpoint to the following:

1<div 2 tw="p-6 h-full w-full flex justify-center items-center" 3 style={{ 4 background: "linear-gradient(133deg, rgb(6, 182, 212) 0%, rgb(59, 130, 246) 45%, rgb(168, 85, 247) 100%)", 5 fontFamily: '"Josefin-Sans"', 6 }} 7> 8 <div tw="rounded p-10 bg-zinc-900 h-full w-full flex flex-col"> 9 <div tw="mt-10 mb-3 flex items-center"> 10 <img 11 width="84" 12 height="84" 13 src={`<LINK_TO_PROFILE_PIC>`} 14 tw="border-2 border-white rounded-full" 15 /> 16 </div> 17 <div tw="mt-16 flex text-6xl leading-normal text-gray-200">{title}</div> 18 <div tw="mt-5 flex text-3xl text-gray-300">{description}</div> 19 <div tw="mt-5 flex items-center text-xl text-gray-300"> 20 <div>Your website</div> 21 <div tw="-mt-2 ml-3">.</div> 22 <div tw="ml-3">{publishDate}</div> 23 </div> 24 </div> 25</div> 26

And voila! You should now end up with the something that looks a lot like the one I posted above! og-image-final

...

That was pretty easy...

I'm hoping that by the end of this you'll find that generating eye-catching OG images in Next.js is actually pretty easy. No more manual labour, and no more worrying about inconsistent images!

I'm looking forward to seeing all the social media posts with fancy images attached to them 🤩. Good luck!