Skip to content
Last updated on February 24, 2023
66 min read

You can use the following code samples to explore using parameters and different content types with @vercel/og. For a get started guide and system requirements, review using @vercel/og.

You can deploy the examples in this guide on your own Vercel account by using the button below:

In this example, your post image is made up of a static SVG logo and the post title. You will pass the post title as a parameter and use TypeScript.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';
 
export const config = {
  runtime: 'edge',
};
 
export default function handler(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
 
    // ?title=<title>
    const hasTitle = searchParams.has('title');
    const title = hasTitle
      ? searchParams.get('title')?.slice(0, 100)
      : 'My default title';
 
    return new ImageResponse(
      (
        <div
          style={{
            backgroundColor: 'black',
            backgroundSize: '150px 150px',
            height: '100%',
            width: '100%',
            display: 'flex',
            textAlign: 'center',
            alignItems: 'center',
            justifyContent: 'center',
            flexDirection: 'column',
            flexWrap: 'nowrap',
          }}
        >
          <div
            style={{
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              justifyItems: 'center',
            }}
          >
            <img
              alt="Vercel"
              height={200}
              src="data:image/svg+xml,%3Csvg width='116' height='100' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M57.5 0L115 100H0L57.5 0z' /%3E%3C/svg%3E"
              style={{ margin: '0 30px' }}
              width={232}
            />
          </div>
          <div
            style={{
              fontSize: 60,
              fontStyle: 'normal',
              letterSpacing: '-0.025em',
              color: 'white',
              marginTop: 30,
              padding: '0 120px',
              lineHeight: 1.4,
              whiteSpace: 'pre-wrap',
            }}
          >
            {title}
          </div>
        </div>
      ),
      {
        width: 1200,
        height: 630,
      },
    );
  } catch (e: any) {
    console.log(`${e.message}`);
    return new Response(`Failed to generate the image`, {
      status: 500,
    });
  }
}

Image generated using title=my post title

In this example, your post image is made up of an avatar user image and the URL of the user's profile. The image is fetched remotely based on the username passed as a parameter.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';
 
export const config = {
  runtime: 'edge',
};
 
export default async function handler(request: NextRequest) {
  const { searchParams } = request.nextUrl;
  const username = searchParams.get('username');
  if (!username) {
    return new ImageResponse(<>Visit with &quot;?username=vercel&quot;</>, {
      width: 1200,
      height: 630,
    });
  }
 
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          fontSize: 60,
          color: 'black',
          background: '#f6f6f6',
          width: '100%',
          height: '100%',
          paddingTop: 50,
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <img
          width="256"
          height="256"
          src={`https://github.com/${username}.png`}
          style={{
            borderRadius: 128,
          }}
        />
        <p>github.com/{username}</p>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Image generated using a github profile dynamic image for username=vercel

If you have a static image asset stored locally, you can also render it in your Open Graph image. Instead of passing a URL to the <img> element, you can pass the image data as an ArrayBuffer or Buffer object to the src attribute of <img>.

This way, the image will be bundled and deployed together with your function, and there will be no external requests made to fetch the image when rendering the Open Graph image.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'edge',
};
 
const image = fetch(new URL('./image.png', import.meta.url)).then((res) =>
  res.arrayBuffer(),
);
 
export default async function handler() {
  const imageData = await image;
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          background: '#f6f6f6',
          width: '100%',
          height: '100%',
          flexDirection: 'column',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <img width="256" height="256" src={imageData} />
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

In this example, your post image is made up of emojis.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'edge',
};
 
export default async function handler() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 100,
          color: 'black',
          background: 'white',
          width: '100%',
          height: '100%',
          padding: '50px 200px',
          textAlign: 'center',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        👋, 🌎
      </div>
    ),
    {
      width: 1200,
      height: 630,
      // Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent' and 'fluentFlat'
      // Default to 'twemoji'
      emoji: 'twemoji',
    },
  );
}

Image generated using emojis 👋, 🌎

In this example, your post image is made up of an SVG image.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'edge',
};
 
export default async function handler() {
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          fontSize: 40,
          color: 'black',
          background: 'white',
          width: '100%',
          height: '100%',
          textAlign: 'center',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <svg fill="black" viewBox="0 0 284 65">
          <path d="M141.68 16.25c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zm117.14-14.5c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zm-39.03 3.5c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9v-46h9zM37.59.25l36.95 64H.64l36.95-64zm92.38 5l-27.71 48-27.71-48h10.39l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10v14.8h-9v-34h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" />
        </svg>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Image generated using the Vercel logo SVG

In this example, your post image is made of a static title styled with a custom font that you provide.

Paste your custom font TYPEWR__.ttf (Available here) under the /assets folder from the project root.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'edge',
};
 
// Make sure the font exists in the specified path:
const font = fetch(new URL('../../assets/TYPEWR__.ttf', import.meta.url)).then(
  (res) => res.arrayBuffer(),
);
 
export default async function handler() {
  const fontData = await font;
 
  return new ImageResponse(
    (
      <div
        style={{
          backgroundColor: 'white',
          height: '100%',
          width: '100%',
          fontSize: 100,
          fontFamily: '"Typewriter"',
          paddingTop: '100px',
          paddingLeft: '50px',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Typewriter',
          data: fontData,
          style: 'normal',
        },
      ],
    },
  );
}

Image generated using the custom font typewriter

In this example, you use Tailwind CSS to style the content for the OG image using a template provided by Tailwind.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'edge',
};
 
export default async function () {
  return new ImageResponse(
    (
      // Modified based on https://tailwindui.com/components/marketing/sections/cta-sections
      <div
        style={{
          height: '100%',
          width: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: 'white',
        }}
      >
        <div tw="bg-gray-50 flex">
          <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">
              <span>Ready to dive in?</span>
              <span tw="text-indigo-600">Start your free trial today.</span>
            </h2>
            <div tw="mt-8 flex md:mt-0">
              <div tw="flex rounded-md shadow">
                <a
                  href="#"
                  tw="flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-5 py-3 text-base font-medium text-white"
                >
                  Get started
                </a>
              </div>
              <div tw="ml-3 flex rounded-md shadow">
                <a
                  href="#"
                  tw="flex items-center justify-center rounded-md border border-transparent bg-white px-5 py-3 text-base font-medium text-indigo-600"
                >
                  Learn more
                </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Image generated using tailwind css

In this example, your post image uses different languages.

Create an og route in /pages/api and paste the following code:

pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
 
export const config = {
  runtime: 'edge',
};
 
export default async function handler() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 40,
          color: 'black',
          background: 'white',
          width: '100%',
          height: '100%',
          padding: '50px 200px',
          textAlign: 'center',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        👋 Hello नमस्ते こんにちは สวัสดีค่ะ 안녕 добрий день Hallá
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Image generated using other languages

Note: Please note that right to left languages are not currently supported.

In this example, you use HMAC hash to encrypt the parameters passed to the OG image API endpoint so that only encrypted parameters can be passed to the endpoint for security purposes.

In addition to the API endpoint, you will create a front-end dynamic route that generates a link with a token to access the endpoint.

This is the directory structure for these files:

First, create an encrypted route in /pages/api and paste the following code:

pages/api/encrypted.tsx
import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';
 
export const config = {
  runtime: 'edge',
};
 
const key = crypto.subtle.importKey(
  'raw',
  new TextEncoder().encode('my_secret'),
  { name: 'HMAC', hash: { name: 'SHA-256' } },
  false,
  ['sign'],
);
 
function toHex(arrayBuffer: ArrayBuffer) {
  return Array.prototype.map
    .call(new Uint8Array(arrayBuffer), (n) => n.toString(16).padStart(2, '0'))
    .join('');
}
 
export default async function handler(request: NextRequest) {
  const { searchParams } = request.nextUrl;
 
  const id = searchParams.get('id');
  const token = searchParams.get('token');
 
  const verifyToken = toHex(
    await crypto.subtle.sign(
      'HMAC',
      await key,
      new TextEncoder().encode(JSON.stringify({ id })),
    ),
  );
 
  if (token !== verifyToken) {
    return new Response('Invalid token.', { status: 401 });
  }
 
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          fontSize: 40,
          color: 'black',
          background: 'white',
          width: '100%',
          height: '100%',
          padding: '50px 200px',
          textAlign: 'center',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <h1>Card generated, id={id}.</h1>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}

Then, you need to create a frontend component that can take an id query parameter, which will be passed to the API route you created above.

Create the dynamic route [id] under /pages/encrypted and paste the following code:

pages/encrypted/[id].tsx
// This SSG page generates the token to prevent generating OG images with random parameters (`id`).
import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next';
import { ParsedUrlQuery } from 'querystring';
import { createHmac } from 'node:crypto';
 
interface IParams extends ParsedUrlQuery {
  id: string;
}
 
type PageProps = {
  id: string;
  token: string;
};
 
export const getStaticProps: GetStaticProps = async (context) => {
  const { id } = context.params as IParams;
  const hmac = createHmac('sha256', 'my_secret');
  hmac.update(JSON.stringify({ id: id }));
  const token = hmac.digest('hex');
 
  return {
    props: {
      id: id,
      token,
    },
  };
};
 
export function getStaticPaths() {
  return {
    paths: [
      { params: { id: 'a' } },
      { params: { id: 'b' } },
      { params: { id: 'c' } },
    ],
    fallback: false,
  };
}
 
export default function Page({ id, token }: PageProps) {
  return (
    <div>
      <h1>Encrypted Open Graph Image.</h1>
      <p>Only /a, /b, /c with correct tokens are accessible:</p>
      <a
        href={`/api/encrypted?id=${id}&token=${token}`}
        target="_blank"
        rel="noreferrer"
      >
        <code>
          /api/encrypted?id={id}&token={token}
        </code>
      </a>
    </div>
  );
}

Run your project locally and browse to http://localhost/encrypted/a(b or c will also work).

Click on the generated link to be directed to the generated image.

Image generated using /api/encrypted?id=a&token=634dd7e46ec814fb105074b73e26755b0d9966c031dca05d7e7cc65a1619058e

In your actual implementation, you will use the code in /pages/encrypted/[id].tsx with a page to create your post html that will look like this:

In your actual implementation, you will use the code in /app/encrypted/[id]/page.tsx with a page to create your post html that will look like this: