Hono OG Middleware: Generate Dynamic OG Images at the Edge
Build a Hono middleware that generates OG images at the edge using Satori. Covers Cloudflare Workers, Deno Deploy, and Bun with full code examples.
Why Hono for OG image generation
Hono is a lightweight web framework that runs everywhere — Cloudflare Workers, Deno Deploy, Bun, Node.js, and AWS Lambda. Its tiny footprint and edge-first design make it perfect for OG image generation endpoints that need to be fast and globally distributed.
By building OG image generation as a Hono middleware, you can add dynamic social share images to any Hono-based project with minimal code. The middleware intercepts requests to /og/* routes and returns generated PNG images.
Basic Hono OG image route
// src/index.ts
import { Hono } from 'hono';
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
const app = new Hono();
app.get('/og/:title', async (c) => {
const title = decodeURIComponent(c.req.param('title'));
const svg = await satori(
{
type: 'div',
props: {
style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
background: 'linear-gradient(135deg, #0a0a0a, #1a1a2e)',
width: '100%',
height: '100%',
padding: '60px',
},
children: [
{
type: 'div',
props: {
style: {
fontSize: 64,
fontWeight: 900,
color: '#ffffff',
lineHeight: 1.2,
},
children: title,
},
},
{
type: 'div',
props: {
style: {
fontSize: 24,
color: '#8b5cf6',
marginTop: 'auto',
},
children: 'mysite.com',
},
},
],
},
},
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: await loadFont(), // Your font loading logic
weight: 900,
style: 'normal',
},
],
}
);
const png = new Resvg(svg).render().asPng();
return c.body(png, 200, {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400, s-maxage=604800',
});
});
export default app;Building it as reusable middleware
Extract the OG generation logic into a reusable middleware factory:
// middleware/og.ts
import { createMiddleware } from 'hono/factory';
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
interface OgOptions {
brandName: string;
brandColor: string;
fontData: ArrayBuffer;
}
export function ogMiddleware(options: OgOptions) {
return createMiddleware(async (c, next) => {
// Only handle /og/* routes
if (!c.req.path.startsWith('/og/')) {
return next();
}
const params = new URL(c.req.url).searchParams;
const title = params.get('title') || 'Untitled';
const subtitle = params.get('subtitle') || '';
const svg = await satori(
{
type: 'div',
props: {
style: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
background: '#0a0a0a',
width: '100%',
height: '100%',
padding: '60px',
},
children: [
{
type: 'div',
props: {
style: { fontSize: 56, fontWeight: 900, color: '#fff' },
children: title,
},
},
subtitle && {
type: 'div',
props: {
style: { fontSize: 28, color: '#a1a1aa', marginTop: 16 },
children: subtitle,
},
},
{
type: 'div',
props: {
style: {
fontSize: 24,
color: options.brandColor,
marginTop: 'auto',
},
children: options.brandName,
},
},
].filter(Boolean),
},
},
{
width: 1200,
height: 630,
fonts: [{ name: 'Inter', data: options.fontData, weight: 900 }],
}
);
const png = new Resvg(svg).render().asPng();
return c.body(png, 200, {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400',
});
});
}// src/index.ts — using the middleware
import { Hono } from 'hono';
import { ogMiddleware } from './middleware/og';
const app = new Hono();
app.use('/*', ogMiddleware({
brandName: 'My SaaS',
brandColor: '#8b5cf6',
fontData: await fetch('https://...').then(r => r.arrayBuffer()),
}));
// Your regular routes
app.get('/', (c) => c.html('<h1>Home</h1>'));
// OG images are now available at:
// /og/?title=My+Blog+Post&subtitle=A+guide+to+something
export default app;Deploying to Cloudflare Workers
For Cloudflare Workers, you'll need @resvg/resvg-wasm instead of the Node.js native module:
// wrangler.toml name = "og-generator" main = "src/index.ts" compatibility_date = "2024-01-01" [build] command = "npm run build" # Bundle font file as static asset [[rules]] type = "Data" globs = ["**/*.ttf"]
import { initWasm, Resvg } from '@resvg/resvg-wasm';
import resvgWasm from '@resvg/resvg-wasm/index_bg.wasm';
let initialized = false;
app.get('/og', async (c) => {
if (!initialized) {
await initWasm(resvgWasm);
initialized = true;
}
// ... rest of generation logic
});Reference OG images in your HTML
<meta property="og:image" content="https://mysite.com/og/?title=My+Post+Title" /> <meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="630" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:image" content="https://mysite.com/og/?title=My+Post+Title" />
Test your Hono-generated OG images
After deploying your Hono OG middleware, verify the images render correctly on every platform with OGFixer.
Check your OG images →