Bun + Hono Open Graph Tags: Fast OG Metadata for Modern APIs

Add Open Graph meta tags to Bun + Hono applications — SSR HTML rendering, dynamic OG endpoints, and Satori integration for OG image generation.

Why Bun + Hono is ideal for OG tags

Bun is an all-in-one JavaScript runtime — package manager, bundler, test runner, and server — that outperforms Node.js in raw throughput. Hono is a lightweight web framework that runs on Bun, Cloudflare Workers, Deno, and Node.js. Together they form a minimal-dependency stack for building server-rendered web apps that embed Open Graph meta tags directly in the HTML.

Because Bun + Hono renders HTML on the server, social crawlers from Twitter, LinkedIn, Discord, and Slack see your OG tags in the initial HTTP response — no JavaScript execution required. This guide covers three patterns: static HTML pages, dynamic routes with database data, and a dedicated OG image endpoint using Satori.

Static HTML with OG tags in Hono

The simplest approach — return HTML with hard-coded OG tags from a Hono route:

// server.ts
import { Hono } from "hono";
import { html } from "hono/html";

const app = new Hono();

app.get("/", (c) => {
  return c.html(html`<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Bun App — Fast by Default</title>
    <meta name="description" content="Built with Bun and Hono for blazing speed." />

    <meta property="og:type"        content="website" />
    <meta property="og:title"       content="My Bun App — Fast by Default" />
    <meta property="og:description" content="Built with Bun and Hono for blazing speed." />
    <meta property="og:image"       content="https://yourdomain.com/og/home.png" />
    <meta property="og:url"         content="https://yourdomain.com/" />

    <meta name="twitter:card"        content="summary_large_image" />
    <meta name="twitter:title"       content="My Bun App — Fast by Default" />
    <meta name="twitter:description" content="Built with Bun and Hono for blazing speed." />
    <meta name="twitter:image"       content="https://yourdomain.com/og/home.png" />
  </head>
  <body>
    <h1>Welcome</h1>
  </body>
</html>`);
});

export default { port: 3000, fetch: app.fetch };

Dynamic OG tags from database data

For blog posts or product pages, fetch data from your database inside the route handler and interpolate it into the HTML response. Hono's JSX support (hono/jsx) makes this even cleaner:

// src/routes/blog.tsx  (using Hono JSX renderer)
import { Hono } from "hono";
import { jsxRenderer } from "hono/jsx-renderer";

const blog = new Hono();

// Example DB helper
async function getPost(slug: string) {
  // Replace with your Bun SQLite / Turso / Drizzle ORM call
  const db = await import("./db");
  return db.posts.findFirst({ where: { slug } });
}

blog.get("/:slug", async (c) => {
  const slug = c.req.param("slug");
  const post = await getPost(slug);

  if (!post) return c.notFound();

  const ogImage = post.ogImage
    ?? `https://yourdomain.com/api/og?title=${encodeURIComponent(post.title)}`;
  const canonicalUrl = `https://yourdomain.com/blog/${slug}`;

  return c.html(
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>{post.title} | My Blog</title>
        <meta name="description" content={post.excerpt} />

        <meta property="og:type"        content="article" />
        <meta property="og:title"       content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta property="og:image"       content={ogImage} />
        <meta property="og:url"         content={canonicalUrl} />

        <meta name="twitter:card"  content="summary_large_image" />
        <meta name="twitter:image" content={ogImage} />
        <link rel="canonical" href={canonicalUrl} />
      </head>
      <body>
        <article>
          <h1>{post.title}</h1>
          <p>{post.excerpt}</p>
        </article>
      </body>
    </html>
  );
});

export default blog;

Bun's native bun:sqlite module makes local SQLite queries extremely fast — you can serve a fully server-rendered page with dynamic OG tags in under 5 ms.

OG image endpoint with Satori on Bun

Add a /api/og endpoint that uses Satori to generate a PNG from JSX and a title string. Bun handles Satori exceptionally fast due to its native JSC engine:

// src/routes/og-image.ts
import { Hono } from "hono";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";

const ogRoute = new Hono();

// Load font once at startup
const fontFile = Bun.file("./public/fonts/Inter-Bold.ttf");
const fontData = await fontFile.arrayBuffer();

ogRoute.get("/", async (c) => {
  const title = c.req.query("title") ?? "My Site";

  const svg = await satori(
    {
      type: "div",
      props: {
        style: {
          width: "100%", height: "100%",
          background: "linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%)",
          display: "flex", alignItems: "center", justifyContent: "center",
          padding: "80px",
        },
        children: {
          type: "h1",
          props: {
            style: { color: "white", fontSize: 64, fontWeight: 700, textAlign: "center" },
            children: title,
          },
        },
      },
    },
    {
      width: 1200, height: 630,
      fonts: [{ name: "Inter", data: fontData, weight: 700, style: "normal" }],
    }
  );

  const png = new Resvg(svg).render().asPng();

  return new Response(png, {
    headers: {
      "Content-Type": "image/png",
      "Cache-Control": "public, max-age=86400",
    },
  });
});

export default ogRoute;

Wiring it together: main entry point

Compose routes and start the Bun server:

// index.ts
import { Hono } from "hono";
import blog from "./src/routes/blog";
import ogRoute from "./src/routes/og-image";

const app = new Hono();

app.route("/blog", blog);
app.route("/api/og", ogRoute);

// Start with Bun
export default { port: 3000, fetch: app.fetch };

Run with bun run index.ts. Every blog post at /blog/:slug gets a dynamically generated OG image from /api/og?title=... if no stored image exists.

Verify your Bun + Hono OG tags

Paste any URL into OGFixer to see live previews on Twitter, LinkedIn, Discord, and Slack — no login required.

Related guides