---
title: "OG Image Generation: The Complete Customization Guide"
description: Learn how to use custom fonts in your Vercel OG image, plus emoji, external images, non-Latin text, dynamic titles, and article headlines.
url: /kb/guide/using-custom-font
canonical_url: "https://vercel.com/kb/guide/using-custom-font"
published: 2025-11-04
last_updated: 2026-09-02
authors: DX Team
related:
  - /docs/og-image-generation
  - /kb/guide/encrypting-parameters
  - /docs/og-image-generation/og-image-api
  - /docs/og-image-generation/examples
  - /docs/deployments/og-preview
  - /kb/guide/using-svg-image
  - /kb/guide/using-tailwind
  - /docs/cdn
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---
<!-- docsgraph:related -->
## Related pages

> **For AI agents:** Follow these links to understand how this page connects to the rest of the Vercel ecosystem. For the full cross-link map (inbound, outbound, prerequisites, and semantic neighbors), see the .graph.md link below.

- [Introducing OG Image Generation: Fast, dynamic social card images at the Edge](https://vercel.com/blog/introducing-vercel-og-image-generation-fast-dynamic-social-card-images?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related)
- [Next.js on Vercel](https://vercel.com/docs/frameworks/full-stack/nextjs?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — Vercel is the native Next.js platform, designed to enhance the Next.js experience.
- [opengraph-image and twitter-image](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — API Reference for the Open Graph Image and Twitter Image file conventions.
- [ImageResponse](https://nextjs.org/docs/app/api-reference/functions/image-response?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — API Reference for the ImageResponse constructor.
- [Displaying headlines in social previews with Vercel OG](https://vercel.com/kb/guide/displaying-article-headlines-in-social-previews?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — Twitter/X is planning to remove headlines from social previews. To get around this limitation, Vercel OG offers a way to
- [Using emoji in your OG image](https://vercel.com/kb/guide/using-emoji-in-image?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — Learn how to use emojis to generate an OG image.
- [Using an external image as OG image](https://vercel.com/kb/guide/using-an-external-dynamic-image?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — Learn how to pass the username as a URL parameter to pull an external profile image for the image generation.
- [Using dynamic text as your OG Image](https://vercel.com/kb/guide/dynamic-text-as-image?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — Learn how to pass the image title as a URL parameter.
- [Using languages in your OG image](https://vercel.com/kb/guide/using-different-languages?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=related) — Learn how to use other languages in the text of your OG image.

Full cross-link map for this page: [/kb/guide/using-custom-font.graph.md](/kb/guide/using-custom-font.graph.md?from=related&source_path=%2Fkb%2Fguide%2Fusing-custom-font&source_site=vercel-kb&relationship=graph)
<!-- /docsgraph:related -->


## Customize a Vercel OG image with custom fonts

Every page on your site can have its own social card, rendered on request instead of designed by hand. [`@vercel/og`](https://vercel.com/docs/og-image-generation) builds that Open Graph (OG) image from JSX and CSS, and by default it returns plain text in a single bundled font. Customizing a Vercel OG image means changing the JSX you pass in, the options you set on `ImageResponse`, or both.

Every example below builds on the same route handler. Create `app/api/og/route.tsx` and paste the following code:

```tsx
import { ImageResponse } from 'next/og';
// App router includes @vercel/og. No need to install it.

export async function GET() {
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          fontSize: 60,
          color: 'black',
          background: 'white',
          width: '100%',
          height: '100%',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}
```

Run `pnpm dev` and browse to `http://localhost:3000/api/og` to see the rendered PNG.

## How to use a custom font in your Vercel OG image

`@vercel/og` ships with one default font, so any other typeface has to be passed in as font data through the `fonts` option. You have two ways to supply it, depending on whether the font lives on Google Fonts or in your repository.

### Load a font from Google Fonts at request time

Google Fonts can return a subset of a font containing only the characters you're rendering, which keeps the payload small. Fetch the stylesheet, extract the font file URL, and pass the result to `ImageResponse`:

```tsx
import { ImageResponse } from 'next/og';

async function loadGoogleFont(font: string, text: string) {
  const url = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
  const css = await (await fetch(url)).text();
  const resource = css.match(
    /src: url\((.+)\) format\('(opentype|truetype)'\)/,
  );

  if (resource) {
    const response = await fetch(resource[1]);
    if (response.status == 200) {
      return await response.arrayBuffer();
    }
  }

  throw new Error('failed to load font data');
}

export async function GET() {
  const text = 'Hello world!';

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          backgroundColor: 'white',
          height: '100%',
          width: '100%',
          fontSize: 100,
          fontFamily: 'Geist',
          paddingTop: '100px',
          paddingLeft: '50px',
        }}
      >
        {text}
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Geist',
          data: await loadGoogleFont('Geist', text),
          style: 'normal',
        },
      ],
    },
  );
}
```

The `text` parameter on the stylesheet request is what triggers subsetting, so pass the same string you're rendering. The `name` you give the font has to match the `fontFamily` value in your styles.

### Load a font file from your repository

For a licensed font or one that isn't on Google Fonts, read the file from disk once at module scope so it isn't re-read on every request:

```tsx
import { ImageResponse } from 'next/og';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

const geistBold = await readFile(join(process.cwd(), 'assets/Geist-Bold.ttf'));

export async function GET() {
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          backgroundColor: 'white',
          height: '100%',
          width: '100%',
          fontSize: 100,
          fontFamily: 'Geist',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Geist',
          data: geistBold,
          weight: 700,
          style: 'normal',
        },
      ],
    },
  );
}
```

Bundled fonts count toward the 500KB bundle limit, which also covers your JSX, CSS, and images. A single weight of a subset font typically lands well under it.

### Troubleshoot failed to load font data errors

The Google Fonts helper throws `failed to load font data` when it can't return font data. Three causes account for nearly every instance:

- **The family name doesn't match Google Fonts:** The value passed to `loadGoogleFont` has to match the family name on Google Fonts, with `+` in place of spaces, as in `Playfair+Display`.
  
- **The stylesheet returned woff2:** The regex matches `opentype` and `truetype` sources only, and `@vercel/og` can't parse `woff2` in any case. Send the request exactly as written, because the format Google Fonts returns varies with the headers you send.
  
- **The font file request failed:** The helper only returns data on a `200` response, so a rate-limited or redirected request falls through to the error.
  

Log `css` before the regex runs to see which of the three you're hitting. The rest of the customization happens through the same `ImageResponse` options.

## How to add emoji to an OG image

Emoji render as images rather than glyphs, so `@vercel/og` substitutes them from an emoji set that you choose with the `emoji` option. Set it alongside `width` and `height`:

```tsx
import { ImageResponse } from 'next/og';

export async function GET() {
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          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' and 'openmoji'
      // Defaults to 'twemoji'
      emoji: 'twemoji',
    },
  );
}
```

The option is global for the image, so a single route renders one emoji style throughout.

## How to use an external image URL in your Vercel OG image

Any absolute image URL can go in an `<img>` tag, which lets you build a card around content you don't control, such as a profile picture. Read the identifier from a query parameter and interpolate it into the `src`:

```tsx
import { ImageResponse } from 'next/og';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  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,
    },
  );
}
```

Set `width` and `height` on the `<img>` tag, which Satori recommends for embedded images. Handle the missing-parameter case as well, so a malformed link still returns a card instead of an error.

## How to render non-Latin languages in an OG image

Non-Latin text renders from the same JSX as Latin text, with no `fonts` option required:

```tsx
import { ImageResponse } from 'next/og';

export async function GET() {
  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',
        }}
      >
        👋 Hello 你好 नमस्ते こんにちは สวัสดีค่ะ 안녕 добрий день Hallá
      </div>
    ),
    {
      width: 1200,
      height: 630,
    },
  );
}
```

Right-to-left languages aren't supported by [Satori](https://github.com/vercel/satori#css), the layout engine underneath `@vercel/og`, so their text won't lay out correctly.

To render a script in a specific typeface rather than the default, pass the font through the `fonts` option. Whatever the script, the text itself changes per page, which is what URL parameters are for.

## How to pass dynamic text to your Vercel OG image

A query parameter turns one route into a card for every page on your site. Read the parameter, cap its length, and fall back to a default when it's missing:

```tsx
import { ImageResponse } from 'next/og';

export async function GET(request: Request) {
  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',
            height: '100%',
            width: '100%',
            display: 'flex',
            textAlign: 'center',
            alignItems: 'center',
            justifyContent: 'center',
            flexDirection: 'column',
            flexWrap: 'nowrap',
          }}
        >
          <div
            style={{
              fontSize: 60,
              letterSpacing: '-0.025em',
              color: 'white',
              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,
    });
  }
}
```

Reference it from your page metadata as `/api/og?title=my%20post%20title`, using an absolute URL so social platforms can fetch it. The `slice(0, 100)` guard matters, because an unbounded title overflows the card.

To stop anyone from generating cards with arbitrary text on your domain, see [encrypting parameters](https://vercel.com/kb/guide/encrypting-parameters).

## How to show article headlines in social previews with Vercel OG

Rendering the headline inside the image keeps it readable in every link preview, regardless of how a social platform treats the page title. Combine a background image, a custom font, and a dynamic title in one route:

```tsx
import { ImageResponse } from 'next/og';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

const headlineFont = await readFile(
  join(process.cwd(), 'assets/headline-italic-700.ttf'),
);

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  // Fetch the title and image from your database.
  // They're hardcoded here.
  const title = searchParams.get('title') ?? 'Your article headline';
  const image = '<https://example.com/article-hero.jpg>';

  return new ImageResponse(
    (
      <div
        style={{
          height: '100%',
          width: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'flex-start',
          justifyContent: 'center',
          backgroundImage: `url(${image})`,
          backgroundRepeat: 'no-repeat',
          backgroundSize: 'cover',
          color: 'white',
        }}
      >
        <div
          style={{
            display: 'flex',
            position: 'absolute',
            bottom: 60,
            left: 80,
            fontSize: 50,
            fontFamily: 'Headline',
            maxWidth: 900,
            whiteSpace: 'pre-wrap',
            letterSpacing: -1,
          }}
        >
          {title}
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Headline',
          data: headlineFont,
          weight: 700,
          style: 'italic',
        },
      ],
    },
  );
}
```

Swap the hardcoded values for a lookup against your content source, keyed on the article slug you pass in. The [OG image template](https://vercel.com/templates/next.js/og-cool) is a working version of this pattern if you'd rather start from a repository than build the route yourself.

## Next steps

With the customizations in place, the remaining work is deploying the route so social platforms can fetch the image at an absolute URL. Deploy the project this route lives in at [vercel.com/new](https://vercel.com/new), or start from a preconfigured project in the [template gallery](https://vercel.com/templates).

## Related resources

- [OG Image Generation](https://vercel.com/docs/og-image-generation)
  
- [@vercel/og reference](https://vercel.com/docs/og-image-generation/og-image-api)
  
- [OG image examples](https://vercel.com/docs/og-image-generation/examples)
  
- [Inspecting OG metadata](https://vercel.com/docs/deployments/og-preview)
  
- [Using an SVG image](https://vercel.com/kb/guide/using-svg-image)
  
- [Using Tailwind CSS](https://vercel.com/kb/guide/using-tailwind)
  
- [Encrypting parameters](https://vercel.com/kb/guide/encrypting-parameters)
  

## Frequently asked questions

### What size should a Vercel OG image be?

The recommended size is 1200x630 pixels, which is also the default `width` and `height` on `ImageResponse`, so both options can be omitted at that size. The values you pass become the pixel dimensions of the returned PNG, so keep them consistent with the `og:image:width` and `og:image:height` tags in your page markup.

### How do I test an OG image before sharing the link?

Open the deployment in your Vercel dashboard and select the **Open Graph** tab, which renders previews for Twitter, Slack, Facebook, and LinkedIn from that deployment's [OG metadata](https://vercel.com/docs/deployments/og-preview). For work on the image itself, the [OG Playground](https://og-playground.vercel.app/) renders your JSX in the browser, so you can check a layout without deploying.

### Are Vercel OG images cached?

Yes. `@vercel/og` sets `cache-control: public, immutable, no-transform, max-age=31536000` on every response, so the [CDN](https://vercel.com/docs/cdn) serves repeat requests without re-rendering. Because that header marks the response as immutable, change the URL, typically through a query parameter, when you want a new image for the same page.

### Which font formats does `@vercel/og` support?

The library accepts `ttf`, `otf`, and `woff` font data, and `ttf` or `otf` parse faster than `woff`. `woff2` isn't supported, so a font that renders in the browser can still fail in an OG image. Convert the file to `ttf` before passing it in the `fonts` array.