Skip to content

Generate Dynamic OG Images with Hono + Cloudflare Workers

この記事は英語版のみ公開されています。

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

References