Bun Open Graph Meta Tags: Add OG to Your Bun.js App

How to add og:title, og:image, og:description, and Twitter card meta tags to a Bun application — using Bun.serve, ElysiaJS, or a Bun + React/Next.js setup.

Bun.serve: vanilla HTML with OG tags

Bun's built-in HTTP server (Bun.serve) lets you return HTML responses directly. Inject OG meta tags server-side so scrapers receive them in the initial response:

// server.ts
function renderPage(title: string, description: string, image: string, url: string): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>${title}</title>
  <meta name="description" content="${description}" />
  <meta property="og:title"        content="${title}" />
  <meta property="og:description"  content="${description}" />
  <meta property="og:image"        content="${image}" />
  <meta property="og:url"          content="${url}" />
  <meta property="og:type"         content="article" />
  <meta name="twitter:card"        content="summary_large_image" />
  <meta name="twitter:title"       content="${title}" />
  <meta name="twitter:description" content="${description}" />
  <meta name="twitter:image"       content="${image}" />
  <link rel="canonical" href="${url}" />
</head>
<body>
  <h1>${title}</h1>
</body>
</html>`;
}

Bun.serve({
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url);

    if (url.pathname.startsWith('/blog/')) {
      const slug = url.pathname.replace('/blog/', '');
      // Replace with your data source
      const post = await getPost(slug);
      if (!post) return new Response('Not found', { status: 404 });

      const html = renderPage(
        post.title,
        post.excerpt,
        `https://yourdomain.com/og/${slug}.png`,
        req.url,
      );
      return new Response(html, {
        headers: { 'Content-Type': 'text/html; charset=utf-8' },
      });
    }

    return new Response('Hello from Bun!');
  },
});

ElysiaJS: OG tags in HTML responses

ElysiaJS is the most popular web framework built for Bun. Use it with @elysiajs/html or return raw HTML strings:

// index.ts
import { Elysia } from 'elysia';
import { html } from '@elysiajs/html';

const app = new Elysia()
  .use(html())
  .get('/blog/:slug', async ({ params: { slug } }) => {
    const post = await getPost(slug); // your data fetch

    return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>${post.title}</title>
  <meta property="og:title"       content="${post.title}" />
  <meta property="og:description" content="${post.excerpt}" />
  <meta property="og:image"       content="https://yourdomain.com/og/${slug}.png" />
  <meta property="og:url"         content="https://yourdomain.com/blog/${slug}" />
  <meta property="og:type"        content="article" />
  <meta name="twitter:card"       content="summary_large_image" />
  <meta name="twitter:title"      content="${post.title}" />
  <meta name="twitter:image"      content="https://yourdomain.com/og/${slug}.png" />
  <link rel="canonical" href="https://yourdomain.com/blog/${slug}" />
</head>
<body>
  <h1>${post.title}</h1>
</body>
</html>`;
  })
  .listen(3000);

console.log('Listening on', app.server?.url);

Bun + Next.js

If you're using Bun as the runtime for a Next.js project (bun dev), add OG tags in the generateMetadata export of your page — Next.js handles server-side injection automatically:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: `https://yourdomain.com/og/${params.slug}.png`, width: 1200, height: 630 }],
      type: 'article',
    },
    twitter: {
      card: 'summary_large_image',
      images: [`https://yourdomain.com/og/${params.slug}.png`],
    },
  };
}

Bun-specific gotchas

  • Missing Content-Type header — always set Content-Type: text/html; charset=utf-8 when returning HTML from Bun.serve. Without it, some scrapers may not parse the response correctly.
  • Client-side rendering — if you return a bare JavaScript bundle and render HTML in the browser, OG tags won't be visible to scrapers. Use SSR or a static build.
  • OG image URL must be absolute — always use https://yourdomain.com/..., not relative paths.

Verify your Bun app's OG tags

After deploying, paste your URL into OGFixer to preview how your Bun app's links appear on Twitter, LinkedIn, Slack, and Discord — and catch any OG tag issues before they go live.