ZeroStarterRC

OG Images

Dynamic Open Graph image generation for social media previews.

Overview

ZeroStarter generates dynamic Open Graph images for social media previews using takumi-js. Each page type has its own OG image route.

Endpoints

RoutePurpose
/og/homeLanding page preview
/og/docs/[[...slug]]Documentation page previews
/og/blog/[[...slug]]Blog post previews
/og?section=&title=&description=Generic parameterized preview (hire, résumé)

Note: These routes live at /og, deliberately not under /api/. Because robots.txt disallows /api/, keeping the OG routes off it is what lets social unfurlers fetch the preview images. See robots.txt.

How It Works

The docs and blog routes are prerendered at build time (export const dynamic = "force-static" plus generateStaticParams), /og/home is static with no params, and the generic /og route is force-dynamic so it can render from query params on demand. Every route returns a PNG with immutable cache headers (1 year).

The shared utility at web/next/src/lib/og-image.tsx creates consistent images with:

  • Dark gradient background
  • Page title and description (the title scales down and wraps for long headings)
  • Section label (e.g., "Documentation", "Blog")
  • App name branding

It renders with the render function from takumi-js (not ImageResponse), returning a Response with the PNG bytes. In next.config.ts, both @takumi-rs/core and takumi-js are listed in serverExternalPackages, and outputFileTracingIncludes maps the /og route to the native @takumi-rs/core binary so it ships with that function.

Customizing

To add OG images for a new content source, create a route at web/next/src/app/og/<section>/[[...slug]]/route.tsx that forwards to the shared helper:

import { config } from "@/lib/config"
import { generateOgImage } from "@/lib/og-image"
import { yourSource } from "@/lib/source"

export const dynamic = "force-static"

export async function GET(_req: Request, { params }: { params: Promise<{ slug?: string[] }> }) {
  const { slug } = await params

  return generateOgImage(slug, {
    source: yourSource,
    sectionName: "Your Section",
    defaultTitle: `${config.app.name} - Your Section`,
    defaultDescription: `Your default description`,
  })
}

export function generateStaticParams() {
  return yourSource.generateParams().map((params) => ({
    slug: params.slug ?? [],
  }))
}

For a one-off route (no source), build a React element and pass it to renderOgElement from @/lib/og-image. See web/next/src/app/og/home/route.tsx as a reference. For a parameterized preview driven by query params, use the generic web/next/src/app/og/route.tsx endpoint, which calls renderOgImage with section/title/description (as the hire and résumé pages do).

Note: takumi-js v1 changed the default display from flex to inline. Every flex container in your OG JSX must set display: "flex" explicitly (the existing routes already do this).

Metadata Integration

OG images are automatically referenced in page metadata via generatePageMetadata() in web/next/src/lib/fumadocs.tsx. Each docs and blog page gets a unique OG image URL based on its slug.