Using Fonts with Vercel OG Image Generation

Learn how to load and apply local fonts in dynamic OG images using Vercel OG Image Generation

Posted on: May 12, 2025
Using Fonts with Vercel OG Image Generation
This content has been translated by AI from the original Japanese version.

When generating OG images for your website, font selection is a crucial element for expressing your brand identity. In this tutorial, we'll explore how to load and apply local fonts in dynamic OG images using Vercel OG Image Generation.

Vercel OG and Font Basics

Vercel OG is a powerful library that dynamically generates Open Graph images using JSX. Custom fonts are an important element for tailoring OG images to match your brand.

There are mainly three ways to use fonts with Vercel OG:

  1. Built-in system fonts
  2. Loading custom font files (.ttf, .otf, etc.) directly
  3. Using Google Fonts

In this article, we'll focus on the method for using local fonts, as this provides the most stable results in HonoX projects.

Setting Up Local Fonts

1. Preparing Font Files

First, you need to add the font files you want to use to your project. For example, if you're using Noto Sans JP and Noto Serif JP fonts:

  1. Download font files (.ttf or .otf) from Google Fonts site
  2. Place them in an appropriate directory in your project (e.g., public/fonts/)
public/
  fonts/
    Noto_Sans_JP/
      static/
        NotoSansJP-Regular.ttf
        NotoSansJP-Bold.ttf
        ...
    Noto_Serif_JP/
      static/
        NotoSerifJP-Regular.ttf
        ...

2. Loading Font Files

Next, load the font files for OG image generation:

HLJS TSX
// app/utils/og.tsx or similar file
import fs from "node:fs";
import path from "node:path";
import { ImageResponse } from "@vercel/og";

// Specify local font paths
const notoSansJpRegularPath = path.resolve(
  "./public/fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf"
);
const notoSansJpBoldPath = path.resolve("./public/fonts/Noto_Sans_JP/static/NotoSansJP-Bold.ttf");
const notoSerifJpRegularPath = path.resolve(
  "./public/fonts/Noto_Serif_JP/static/NotoSerifJP-Regular.ttf"
);

// Read font files
const notoSansJpRegularFont = fs.readFileSync(notoSansJpRegularPath);
const notoSansJpBoldFont = fs.readFileSync(notoSansJpBoldPath);
const notoSerifJpRegularFont = fs.readFileSync(notoSerifJpRegularPath);

This code resolves the paths to the font files and reads them as binary data.

Applying Fonts to OG Image Generation

After loading the fonts, apply them to the ImageResponse:

HLJS TSX
// Example OG image generation function
export function generateOgImage(title: string, description: string): ImageResponse {
  return new ImageResponse(
    (
      <div
        style={{
          height: "100%",
          width: "100%",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          backgroundColor: "#f6f6f6",
          fontFamily: "'Noto Sans JP', sans-serif",
        }}
      >
        <h1 style={{ fontSize: 64, fontWeight: 700 }}>{title}</h1>
        <p style={{ fontSize: 32 }}>{description}</p>
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "Noto Sans JP",
          data: notoSansJpRegularFont,
          style: "normal",
          weight: 400,
        },
        {
          name: "Noto Sans JP",
          data: notoSansJpBoldFont,
          style: "normal",
          weight: 700,
        },
        {
          name: "Noto Serif JP",
          data: notoSerifJpRegularFont,
          style: "normal",
          weight: 400,
        },
      ],
    }
  );
}

Font Configuration Explained

The fonts array in the options object of ImageResponse is specified as follows:

HLJS TSX
fonts: [
  {
    name: "Noto Sans JP", // Name to use in CSS font-family
    data: notoSansJpRegularFont, // Binary data of the font
    style: "normal", // Font style
    weight: 400, // Font weight
  },
  // You can specify multiple weights and styles
  {
    name: "Noto Sans JP",
    data: notoSansJpBoldFont,
    style: "normal",
    weight: 700,
  },
  // You can also add different font families
  {
    name: "Noto Serif JP",
    data: notoSerifJpRegularFont,
    style: "normal",
    weight: 400,
  },
];

Implementation Example: Real-World Usage in HonoX

Let's look at a real-world implementation example in a HonoX project. This blog site implements it as follows:

1. Helper Functions and Common Utilities

The app/utils/og.tsx file contains font loading and multiple OG image generation functions:

HLJS TSX
// app/utils/og.tsx
import { ImageResponse } from "@vercel/og";
import fs from "node:fs";
import path from "node:path";
import { LocalizedContent } from "./locale";

// Loading local fonts - using static files instead of variable fonts
const notoSansJpRegularPath = path.resolve(
  "./public/fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf"
);
const notoSansJpBoldPath = path.resolve("./public/fonts/Noto_Sans_JP/static/NotoSansJP-Bold.ttf");
const notoSerifJpRegularPath = path.resolve(
  "./public/fonts/Noto_Serif_JP/static/NotoSerifJP-Regular.ttf"
);

// Reading font files
const notoSansJpRegularFont = fs.readFileSync(notoSansJpRegularPath);
const notoSansJpBoldFont = fs.readFileSync(notoSansJpBoldPath);
const notoSerifJpRegularFont = fs.readFileSync(notoSerifJpRegularPath);

// Helper function for site icon
function getSiteIconBase64(): string {
  try {
    const iconPath = path.resolve("./public/icon.png");
    const iconData = fs.readFileSync(iconPath);
    return `data:image/png;base64,${iconData.toString("base64")}`;
  } catch (error) {
    console.error("Failed to load site icon:", error);
    return ""; // Return empty string if icon can't be loaded
  }
}

// OG image generation function
export function generateContentOgImage(
  locale: string,
  localizedContent: LocalizedContent,
  options: {
    title: string;
    category?: string;
    categoryIcon?: string;
    tags?: string[];
    contentType?: "blog" | "idea";
  }
): ImageResponse {
  const { title, category, categoryIcon, tags = [], contentType = "blog" } = options;

  // JSX content and styling...

  return new ImageResponse(
    (
      <div
        style={{
          // Style settings...
          fontFamily: "'Noto Sans JP', sans-serif",
        }}
      >
        {/* JSX content */}
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "Noto Sans JP",
          data: notoSansJpRegularFont,
          style: "normal",
          weight: 400,
        },
        {
          name: "Noto Sans JP",
          data: notoSansJpBoldFont,
          style: "normal",
          weight: 700,
        },
        {
          name: "Noto Serif JP",
          data: notoSerifJpRegularFont,
          style: "normal",
          weight: 400,
        },
      ],
    }
  );
}

2. Using Fonts in Route Definitions

In an OG image route like app/routes/[locale]/blogs/[slug]/og.webp.tsx, use the helper functions above:

HLJS TSX
// app/routes/[locale]/blogs/[slug]/og.webp.tsx
import { createRoute } from "honox/factory";
import { ssgParams } from "hono/ssg";
import { getMessage, assertValidLocale } from "../../../../utils/locale";
import { generateContentOgImage } from "../../../../utils/og";

export default createRoute(
  ssgParams(() => [
    /* ... */
  ]),
  (c) => {
    const locale = c.req.param("locale");
    const slug = c.req.param("slug");

    try {
      // Validate locale
      assertValidLocale(locale);

      // Get blog post data
      const post = getBlogPost(locale, slug);

      // Get localized content
      const localizedContent = getMessage(locale);

      const { frontmatter } = post;

      // Generate OG image
      return generateContentOgImage(locale, localizedContent, {
        title: frontmatter.title,
        category: frontmatter.category,
        categoryIcon: getCategoryIcon(frontmatter.category),
        tags: frontmatter.tags,
        contentType: "blog",
      });
    } catch (error) {
      return c.notFound();
    }
  }
);

Font Selection Best Practices

1. Using Static Files

Use static weight font files (Regular, Bold, etc.) instead of variable fonts:

HLJS TSX
// Good example: Using static weight font
const notoSansJpRegularPath = path.resolve(
  "./public/fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf"
);

// Example to avoid: Using variable font
const notoSansJpVariablePath = path.resolve(
  "./public/fonts/Noto_Sans_JP/NotoSansJP-VariableFont_wght.ttf"
);

This is because Vercel OG works more optimally with static weight fonts than variable fonts.

2. Loading Only Necessary Weights and Styles

For performance, only load the weights and styles you actually use:

HLJS TSX
// Only load the weights you use
const notoSansJpRegularFont = fs.readFileSync(notoSansJpRegularPath);
const notoSansJpBoldFont = fs.readFileSync(notoSansJpBoldPath);

3. Using Base64 Encoded Images

Images like logos and icons can be embedded with Base64 encoding:

HLJS TSX
// Helper function to Base64 encode images
function getImageBase64(imagePath: string): string {
  try {
    const imageData = fs.readFileSync(path.resolve(imagePath));
    return `data:image/png;base64,${imageData.toString("base64")}`;
  } catch (error) {
    return "";
  }
}

// Usage in JSX
<img src={getImageBase64("./public/icon.png")} width={100} height={100} alt="Icon" />;

Performance Optimization

Font File Size Optimization

There are techniques to make font files smaller:

  1. Using subset fonts (containing only needed characters)
  2. Using tools that optimize fonts for web use

Edge Cases and Considerations

1. Handling Missing Fonts

Implement error handling for when font files can't be found:

HLJS TSX
function loadFontWithFallback(fontPath: string, fallbackFontPath: string): Buffer {
  try {
    return fs.readFileSync(fontPath);
  } catch (error) {
    console.error(`Failed to load font from ${fontPath}, using fallback`);
    try {
      return fs.readFileSync(fallbackFontPath);
    } catch {
      throw new Error("Failed to load both primary and fallback fonts");
    }
  }
}

2. Internationalization Support

For multilingual sites, it's important to select appropriate fonts for each language:

HLJS TSX
function getFontForLocale(locale: string) {
  switch (locale) {
    case "ja":
      return {
        fontFamily: "'Noto Sans JP', sans-serif",
        fontData: [notoSansJpRegularFont, notoSansJpBoldFont],
      };
    case "zh":
      return {
        fontFamily: "'Noto Sans SC', sans-serif",
        fontData: [notoSansSCRegularFont, notoSansSCBoldFont],
      };
    default:
      return {
        fontFamily: "'Inter', sans-serif",
        fontData: [interRegularFont, interBoldFont],
      };
  }
}

Vercel OG Font-Related Limitations

When using Vercel OG, it's good to be aware of some limitations:

  1. Divs with multiple children require display attribute: Explicitly specify display: "flex" or display: "none"
HLJS TSX
// Good example
<div style={{ display: "flex" }}>
  <child1 />
  <child2 />
</div>

// Bad example - may cause errors
<div>
  <child1 />
  <child2 />
</div>
  1. CSS limitations: Some CSS properties are not supported

  2. WebP limitations: For Base64 embedded images, it's safer to use PNG format rather than WebP

Importance of .webp.tsx File Extension

It's important to use the .webp.tsx file extension for OG image routes:

app/routes/[locale]/blogs/[slug]/og.webp.tsx

This extension is not just a convention, but necessary for the build process to properly process the images.

Implementing WebP Conversion After Build

Vercel OG generates PNG internally, but converting these to WebP can improve performance:

HLJS JS
// scripts/convert-webp.js
import sharp from "sharp";
import fs from "fs";
import path from "path";
import glob from "glob";

// Script to convert PNG files to WebP
const convertToWebP = async () => {
  const files = glob.sync("dist/**/*.png");

  for (const file of files) {
    const outputPath = file.replace(".png", ".webp");
    await sharp(file).webp({ quality: 90 }).toFile(outputPath);
    console.log(`Converted ${file} to ${outputPath}`);
  }
};

convertToWebP().catch(console.error);

Conclusion

Using local fonts with Vercel OG allows you to achieve consistent, high-quality typography in your dynamically generated OG images.

Key points learned in this article:

  1. Setting up local fonts: Add font files to your project and load them with fs.readFileSync
  2. Defining font information: Properly specify font name, style, and weight
  3. Performance optimization: Load only necessary fonts and utilize caching
  4. Internationalization support: Select appropriate fonts based on language
  5. Understanding web font limitations: Be aware of Vercel OG's limitations and implement workarounds

By combining these techniques, you can generate professional OG images that are consistent with your brand.