Generating images with Vercel OG and Next.js

Vercel has recently released a library called Vercel OG Image that generates dynamic images that you can embed in your meta tags, the so called social cards, or for any other use really, your imagination is the limit and it’s really easy to set it up in Next.js.

How to set up Vercel OG in a Next.js project

First we start by installing the library :

npm i @vercel/og

Then we will need to create a new API endpoint, we can do that by creating a file inside an API folder in the pages folder:

touch pages/api/og.tsx

I’m using Typescript here but if you want to use JavaScript only make sure to name your file .jsx

In this file we need to declare a handler function that handles the API request, inside it we will return the ImageResponse method from the vercel/og library:

import { ImageResponse } from "@vercel/og";
 
export const config = {
  runtime: "experimental-edge",
};
 
export default function () {
  return new ImageResponse(
    (
      <div
        style={{
          display: "flex",
          width: "100%",
          height: "100%",
          margin: "auto",
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        Hey!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  );
}

So this ImageResponse function will take two parameters one is a React component that returns JSX, and the other is an options object, styles have to be inline and not all HTML elements and CSS properties are supported, you can check a list here.

If you run your app and send a GET request to api/og you should get an image returned:

Adjusting the layout of your image

For defining the layout of your image I suggest you use Vercel’s provided playground and adjust the layout to your liking. My Images will accept emojis and text as parameters:

<div
        style={{
          height: "100%",
          width: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          backgroundColor: "rgb(248, 248, 255)",
          position: "relative",
 
        }}
      >
        <span
          style={{
            position: "absolute",
            top: "50%",
            left: "50%",
            transform: "translate(-50%, -50%)",
            color: "hsl(97, 74%, 47%)",
            opacity: "0.4",
            fontSize: "500px",
            fontFamily: "github_events_icons",
          }}
        >★
         
        </span>
          <span
            style={{
              fontSize: "100px",
              letterSpacing:"10px",
              fontFamily: "NotoColorEmoji",
              textAlign: "center",
              margin:'0 auto 20px auto',
              color: 'red'
            }}
          >👻
          </span>
       
        <div
          style={{
            display: "flex",
            placeItems: "center",
            margin:'0 auto 2% auto',
            width: "70%",
            paddingBottom: "30px",
            borderBottom: "25px solid #8c91f2",
            fontFamily: "P5 Font",
            fontWeight: "bold",
            fontSize: "70px",
            color: "rgb(18, 25, 90)",
           
          }}
        >
          <span
            style={{
              margin:'0 auto',
              textAlign: "center",
            }}
          >Post title</span>
        </div>
      </div>

Once you are happy with your design you can then bring your HTML to your og.tsx file and adjust it accordingly, in my case I wanted to make the emoji field optional, so I added some conditional checking to adjust for its presence or absence:

import { ImageResponse } from "@vercel/og";
 
export const config = {
  runtime: "experimental-edge",
};
 
export default async function handler(req) {
  const { searchParams } = new URL(req.url);
  const title = searchParams.get("title") ?? "Pedro Coelho";
  const emoji = searchParams.get("emoji");
 
  return new ImageResponse(
    (
      <div
        style={{
          height: "100%",
          width: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          backgroundColor: "rgb(248, 248, 255)",
          position: "relative",
 
        }}
      >
        <span
          style={{
            position: "absolute",
            top: "50%",
            left: "50%",
            transform: "translate(-50%, -50%)",
            color: "hsl(97, 74%, 47%)",
            opacity: "0.4",
            fontSize: "500px",
            fontFamily: "github_events_icons",
          }}
        >
          ★
        </span>
        {emoji && (
          <span
            style={{
              fontSize: "100px",
              letterSpacing:"10px",
              fontFamily: "NotoColorEmoji",
              textAlign: "center",
              margin:'0 auto 20px auto',
              color: 'red'
            }}
          >
            {emoji}
          </span>
        )}
        <div
          style={{
            display: "flex",
            placeItems: "center",
            margin:'0 auto 2% auto',
            width: "70%",
            paddingBottom: "30px",
            borderBottom: "25px solid #8c91f2",
            fontFamily: "P5 Font",
            fontWeight: "bold",
            fontSize: `${emoji ? "70px" : "80px"}`,
            color: "rgb(18, 25, 90)",
           
          }}
        >
          <span
            style={{
              margin:'0 auto',
              textAlign: "center",
            }}
          >{title}</span>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
     }
  );
}

However I want to use a custom font for my title and for the background I would like to render a random icon, which also comes from a font, generated with fontello

To generate a font we need to fetch it asynchronously, like this :

const font = fetch(
  new URL("../../public/fonts/Font.ttf", import.meta.url)
).then((res) => res.arrayBuffer());

Then we will need to load it inside our function and declare it in our options object, inside a fonts property that receives an array. This is the final code :

import { ImageResponse } from "@vercel/og";
 
export const config = {
  runtime: "experimental-edge",
};
 
//custom fonts
const font = fetch(
  new URL("../../public/fonts/P5Font.ttf", import.meta.url)
).then((res) => res.arrayBuffer());
 
const icons = fetch(
  new URL("../../public/fonts/github_events_icons.ttf", import.meta.url)
).then((res) => res.arrayBuffer());
 
export default async function handler(req) {
 
//first we need to load the custom fonts
  const fontData = await font;
  const iconData = await icons;
 
 
  const { searchParams } = new URL(req.url);
  const title = searchParams.get("title") ?? "Pedro Coelho";
  const emoji = searchParams.get("emoji");
 
//this is my array of random BG icons
  const iconsArray = [
    "&#xe801;",
    "&#xe802;",
    "&#xe803;",
    "&#xe804;",
    "&#xe805;",
    "&#xe806;",
    "&#xe808;",
    "&#xe809;",
    "&#xe80a;",
    "&#xe80b;",
    "&#xe80c;",
    "&#xe80d;",
    "&#xe80e;",
    "&#xe810;",
  ];
  const randomIcon = iconsArray[Math.floor(Math.random() * iconsArray.length)];
 
//this is used to convert the Fontello characters from Unicode
function convertUnicode(input) {
  return String.fromCodePoint(parseInt(input.replace(/&#x|;/g, ""), 16));
}
 
 
  const renderIcon = convertUnicode(randomIcon);
 
  return new ImageResponse(
    (
      <div
        style={{
          height: "100%",
          width: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          backgroundColor: "rgb(248, 248, 255)",
          position: "relative",
 
        }}
      >
        <span
          style={{
            position: "absolute",
            top: "50%",
            left: "50%",
            transform: "translate(-50%, -50%)",
            color: "hsl(97, 74%, 47%)",
            opacity: "0.4",
            fontSize: "500px",
            fontFamily: "github_events_icons",
          }}
        >
          {renderIcon}
        </span>
        {emoji && (
          <span
            style={{
              fontSize: "100px",
              letterSpacing:"10px",
              fontFamily: "NotoColorEmoji",
              textAlign: "center",
              margin:'0 auto 20px auto',
              color: 'red'
            }}
          >
            {emoji}
          </span>
        )}
        <div
          style={{
            display: "flex",
            placeItems: "center",
            margin:'0 auto 2% auto',
            width: "70%",
            paddingBottom: "30px",
            borderBottom: "25px solid #8c91f2",
            fontFamily: "P5 Font",
            fontWeight: "bold",
            fontSize: `${emoji ? "70px" : "80px"}`,
            color: "rgb(18, 25, 90)",
           
          }}
        >
          <span
            style={{
              margin:'0 auto',
              textAlign: "center",
            }}
          >{title}</span>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "P5 Font",
          data: fontData, /* loaded font is passed down here */
          style: "normal",
        },
        {
          name: "github_events_icons",
          data: iconData,
          style: "normal",
        }
      ],
    }
  );
}

Et voilá! If you run your project locally, you should be able to generate your image by hitting the API at /api/og , here I tested it on Postman:

In the next part I’ll show you how to update your headers and meta-tags so social media like Twitter, Facebook and LinkedIn will display your image.