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:
- Built-in system fonts
- Loading custom font files (.ttf, .otf, etc.) directly
- 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:
- Download font files (.ttf or .otf) from Google Fonts site
- 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:
// 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:
// 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:
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:
// 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:
// 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:
// 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:
// 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:
// 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:
- Using subset fonts (containing only needed characters)
- 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:
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:
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:
- Divs with multiple children require display attribute: Explicitly specify
display: "flex"
ordisplay: "none"
// Good example
<div style={{ display: "flex" }}>
<child1 />
<child2 />
</div>
// Bad example - may cause errors
<div>
<child1 />
<child2 />
</div>
-
CSS limitations: Some CSS properties are not supported
-
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:
// 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:
- Setting up local fonts: Add font files to your project and load them with fs.readFileSync
- Defining font information: Properly specify font name, style, and weight
- Performance optimization: Load only necessary fonts and utilize caching
- Internationalization support: Select appropriate fonts based on language
- 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.