この記事は英語版のみ公開されています。
NOTE Originally published in Japanese on zenn.dev (2025/10/28).
I needed to generate dynamic OG images for my personal project (cinefil.me). So I planned to create an API endpoint to generate an OG image and provide access to it on the user profile page like this:
<meta property="og:image" content="https://api.cinefil.me/api/og-image?userId=123" />
Here’s how to generate dynamic OG images with Hono and Cloudflare Workers.
Background
Initially, I considered using @vercel/og. However, packages that designed for Node.js often fails in Cloudflare Workers due to the different runtime used by V8 isolates. Specifically, @vercel/og relies on Node.js dependencies, making it incompatible in my environment.
I also explored @cloudflare/pages-plugin-vercel-og, which is tailored for Cloudflare Workers. However, as noted in the official documentation, since I’m using Hono for the backend, I couldn’t simply use it as a Pages Functions middleware.
Ultimately, I found a solution at Vinh Pham’s blog: importing only the ImageResponse function from the @cloudflare/pages-plugin-vercel-og package. With this approach, I could generate OG images dynamically for each user’s page.
Requirements
- backend API built with Hono (^4.9.6)
- deploy backend on Cloudflare Workers
Instructions
1. Install Dependencies
npm i @cloudflare/pages-plugin-vercel-og
2. Cloudflare Workers Configuration
name = "cinefil-api"
main = "server/index.ts"
compatibility_date = "2024-12-01"
+ compatibility_flags = ["nodejs_compat"]
...
3. Implement the OG image template
Create an OG image template using JSX, then render it into an image with ImageResponse.
import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api";
import type { Context } from "hono";
export const getOgImageHandler = async (c: Context<{ Bindings: Bindings }>) => {
const options = {
width: 1200,
height: 630,
};
// generate an image from JSX with ImageResponse
return new ImageResponse(<div>...</div>, options)
};
4. Reflect the user’s data to the OG image
Get the user data from the database.
// get userId from query
const userId = c.req.query("userId");
// get data from the database
const result = await c.env.YOUR_DB_BINDING
.prepare(`SELECT avatar, display_name FROM profiles WHERE user_id = ?`)
.bind(userId)
.first();
// reflect data to JSX
return new ImageResponse(
(
<div>
<img src={avatar as string} width="80" height="80" alt={display_name as string} />
<div>{display_name as string}</div>
</div>
),
options
)
5. Add fonts
You can customize the design by using local font files.
Since fs.readFile is not available in the Workers runtime, you must fetch() and convert into an arrayBuffer().
// read font files from /public/fonts/
// ttf instead of woff due to error
const fontData = await fetch(
new URL(`<SITE_URL>/fonts/RobotoMono-SemiBold.ttf`, import.meta.url)
).then((res) => res.arrayBuffer());
// set up ImageResponse option
const options = {
width: 1200,
height: 630,
fonts: [{
name: "RobotoMono",
data: fontData,
style: "normal",
weight: 500,
}],
};
// set up fonts in JSX
return new ImageResponse(
(
<div style={{ fontFamily: "RobotoMono" }}>
<img src={avatar as string} width="80" height="80" alt={display_name as string} />
<div>{display_name as string}</div>
</div>
),
options
)
The Result
This is the result page in my application.
https://cinefil.me/yuri5
You can see the whole code in the GitHub.
https://github.com/chocolat5/utils/tree/master/hono-og-image