Generating dynamic Open Graph images with the @vercel/og library

This post has been translated by DeepL . Please let us know if there are any mistranslations!

overview

after making a blog post available in multiple languages, when changing the language, the text changes but the thumbnail doesn't, so it looks awkward when the content is in English but the image is in Korean. for this reason, I realized that I needed to dynamically generate images, so I researched and found a library provided by vercel.

Setting up dynamic OG images in Next.js

Next.js supports dynamic OG image generation through Vercel's @vercel/og library. this functionality is computed and executed via Vercel Functions.

to install

  • App Router project: No additional package installation required (it's built-in)
    • import { ImageResponse } from "next/og"
  • Pages Router project: npm i @vercel/og Install with the command
    • import { ImageResponse } from "@vercel/og

basic Usage

styling

create your component as you would normally create a component. It's an API because it uses JSX syntax, but you need to create a file extension with jsx or tsx.

you can create it by inserting the style directly like below,

return new ImageResponse(
  <div
    style={{
      height: '100%',
      width: '100%',
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: '#fff',
      fontSize: 32,
      fontWeight: 600,
    }}
  >
    <div style={{ marginTop: 40 }}>Hello, World</div>
  </div>
)

tw props to apply the tailwindCSS syntax.

return new ImageResponse(
  <div
    tw="h-full w-full flex flex-col items-center justify-center bg-white text-xl font-bold"
  >
    <div tw="mt-10">Hello, World</div>
  </div>
)

Pages Router project

/pages/api/og.tsx path and write the following

import { NextRequest } from "next/server";
import { ImageResponse } from "@vercel/og";

export const config = {
  runtime: "edge",
};

export default async function handler(request: NextRequest) {
  const { searchParams } = request.nextUrl;
  const title = searchParams.get("title");
  
  return new ImageResponse(
    (
      <div tw="flex flex-col w-full h-full items-center justify-center bg-white">
        <div tw="bg-gray-50 flex w-full">
          <div tw="flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8">
            <h2 tw="flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-gray-900 text-left">
              {title}
            </h2>
            <div tw="mt-8 flex md:mt-0">
              <div tw="flex rounded-md shadow">
                <a tw="flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-5 py-3 text-base font-medium text-white">
                  시작하기
                </a>
              </div>
              <div tw="ml-3 flex rounded-md shadow">
                <a tw="flex items-center justify-center rounded-md border border-transparent bg-white px-5 py-3 text-base font-medium text-indigo-600">
                  더 알아보기
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Since Pages Routrer does not support the Node.js runtime, the following settings must be additionally applied so that the Vercel function runs in the Edge runtime.

export const config = {
  runtime: "edge",
};

App Router project

/app/api/og/route.tsx file and write as follows. The path and the way it is written are different from Pages Router, but the value returned is the same.

export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl;
  const title = searchParams.get("title");

  // 이하 생략... 
}

as you can see in the code, you can dynamically get and display the value via searchParams.

const title = searchParams.get("title");

this is what you'll see in your browser when you make the following API call /api/og?title=WOW

Image.png

adding a custom font

let's add a custom font for a more OG look. given that the font file is located under /public/fonts/, we can write something like this

import { NextRequest } from "next/server";
import { ImageResponse } from "next/og";
import { join } from "path";
import { readFileSync } from "fs";

export async function GET(request: NextRequest) {
  // 생략...
  const fontPath = join(process.cwd(), "public", "fonts", "WONTitle.ttf");
  const fontData = readFileSync(fontPath);
  
  return new ImageResponse(
    (
      <div>
      // 생략...
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "WONTitle",
          data: fontData,
        },
      ],
    },
  );
}

adding an image

you can also add an image by importing it. if an external URL exists, you can just paste it in, or if you have a locally available image, you can use something like this

const logoPath = join(
  process.cwd(),
  "public",
  "images",
  "logo",
  `${logo}.png`,
);
const logoBuffer = readFileSync(logoPath);
const logoBase64 = `data:image/png;base64,${logoBuffer.toString("base64")}`;

// ... 생략

return new ImageResponse((
  <img
    alt="og thumbnail"
    tw="absolute -translate-x-1/2 -translate-y-1/2 "
    src={logoBase64}
    width={logoWidth}
    height={logoHeight}
    style={{
      opacity: 0.6,
    }}
/>), ...)

the result

AnimatedImage.gif

we can now dynamically generate thumbnails with the same style but different text for different languages. i used to have to go into the image editor to generate thumbnails every time, but I don't think I'll ever have to do that again.

🔎 See

❤️ 0
🔥 0
😎 0
⭐️ 0
🆒 0