Dynamic Open Graph (OG) image generation service for blogs and websites, built with Next.js 16 on Vercel Edge Runtime.
Demo: https://og.tuuhub.com
⚠️ Important: The demo site is for demonstration purposes ONLY. Please deploy your OWN instance for production use.
- Beautiful Default Background - Stunning starry sky image when no background is provided
- Custom Background Support - Use your own images (PNG, JPG, JPEG, GIF)
- Switchable Pixel Fonts - Zpix + Geist Pixel variants (all localized in
public/fonts) - Frosted Glass Effect - Enhanced readability with backdrop blur overlay
- Responsive Typography - Dynamic font sizing based on title length
- Edge Runtime - Fast generation with global CDN caching
- CJK Support - Full support for Chinese, Japanese, and Korean characters
For most users, use a random custom API path:
OG_API_PATH=og_a9f4k2m8x7p1
# Use a random path (8-16 chars, letters/numbers/_/-).
Use
https://example.com/api/<OG_API_PATH>as the API endpoint.
If you only need OG image APIs (no landing page), enable API-only mode:
OG_API_ONLY=true
Behavior when enabled:
- Returns
404for non-API pages (for example/). - Keeps all API routes available (
/api/*). - Keeps OG-required static assets available:
/default-bg.jpg,/fonts/*,/_next/*.
- Framework: Next.js 16 with App Router
- Runtime: Vercel Edge Runtime
- Image Generation: @vercel/og (Satori)
- Language: TypeScript
- Deployment: Vercel
- Supported Image Formats: PNG, JPG, JPEG, GIF
- Unsupported Formats: WebP, AVIF, SVG (@vercel/og limitation)
- Config Endpoint:
/api/og-configis disabled by default; enable withOG_ENABLE_CONFIG_ENDPOINT=trueonly when needed - Debug Endpoint: keep
/api/debugdisabled in production
Developer Docs (Advanced)
# Install dependencies
bun install
# Start development server
bun run dev
# Build for production
bun run build
# Run all tests
bun testVisit http://localhost:3000/api/og?title=Hello&site=Blog to test (default path).
Base pattern:
GET /api/<OG_API_PATH>
- No
OG_SECRET: endpoint is usually/api/og - With
OG_SECRET: endpoint is auto-derived (for example/api/og_xxxxxxxx) and signature check is enabled (advanced mode) - Optional: set
OG_API_PATHto pin a stable path
To inspect current endpoint and signature requirement temporarily:
OG_ENABLE_CONFIG_ENDPOINT=true
GET /api/og-configRequired:
title: article titlesite: site name
Optional:
excerpt,author,date,imagetheme(pixelormodern)pixelFont(used whentheme=pixel)sig: optional; required only whenOG_SECRETorOG_SIGNATURE_SECRETis setexp: optional Unix timestamp for expiring signatures
Unsigned example:
https://your-domain.com/api/og?title=Hello&site=Blog
Signed example:
https://your-domain.com/api/your-random-key?title=Hello&site=Blog&sig=<signature>
bun scripts/sign-og-url.mjs \
--url "https://your-domain.com/api/your-random-key?title=Hello&site=Blog" \
--exp-seconds 604800Secret priority:
--secret > OG_SIGNATURE_SECRET > OG_SECRET
| Variable | Required | Purpose | Example |
|---|---|---|---|
OG_SECRET |
No | Advanced mode: auto-derive API path and enable signature validation | OG_SECRET=replace-with-long-random-secret |
OG_API_PATH |
Recommended | Set custom API path (random string recommended) | OG_API_PATH=og_myblog |
OG_API_ALLOW_LEGACY_PATH |
No | Set true to allow legacy /api/og in advanced setups |
OG_API_ALLOW_LEGACY_PATH=true |
OG_SIGNATURE_SECRET |
No | Explicit signature secret (defaults to OG_SECRET) |
OG_SIGNATURE_SECRET=another-long-secret |
OG_API_ONLY |
No | Set true to disable non-API frontend routes |
OG_API_ONLY=true |
OG_ENABLE_DEBUG |
No | Set true to enable /api/debug in production |
OG_ENABLE_DEBUG=false |
OG_ENABLE_CONFIG_ENDPOINT |
No | Set true to expose /api/og-config (disabled by default) |
OG_ENABLE_CONFIG_ENDPOINT=true |
If neither OG_SECRET nor OG_SIGNATURE_SECRET is set, unsigned URLs work.
Use OG_API_PATH direct URLs by default. If signature mode is enabled, generate signatures on the server side and avoid short-lived signatures for published pages.
If you're using the Attegi theme for Ghost, OG image generation is built-in. Simply configure your OG service URL in the theme settings.
For App Router pages (supports dynamic params), use generateMetadata:
import type { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
const title = 'Hello World';
const siteName = 'My Blog';
const ogEndpoint = 'https://your-domain.com/api/your-random-key';
const ogUrl = `${ogEndpoint}?title=${encodeURIComponent(title)}&site=${encodeURIComponent(siteName)}`;
return {
openGraph: {
images: [{ url: ogUrl, width: 1200, height: 630 }],
},
};
}Add to your page frontmatter or layout:
---
const ogEndpoint = 'https://your-domain.com/api/your-random-key';
const ogImage = `${ogEndpoint}?title=${encodeURIComponent(title)}&site=${encodeURIComponent(siteName)}`;
---
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />Add to your template:
{{ $title := .Title }}
{{ $site := .Site.Title }}
{{ $ogPath := "your-random-key" }}
{{ $ogImage := printf "https://your-domain.com/api/%s?title=%s&site=%s" $ogPath (urlquery $title) (urlquery $site) }}
<meta property="og:image" content="{{ $ogImage }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />For other frameworks and platforms, set og:image meta tags to the generated URL (see the Open Graph protocol).
- Image Size: 1200×630px
- Font: Zpix + Geist Pixel (
geist-square,geist-circle,geist-line,geist-triangle,geist-grid)
Replace /public/default-bg.jpg with your own 1200×630px image.
Edit app/lib/pixel-fonts.ts to add/remove local font options, then place corresponding TTF files under public/fonts.
MIT
- Default background: Cluster of Stars by Paul Lichtblau on Unsplash
- Zpix pixel font by SolidZORO
- Geist Pixel by Vercel