A high-performance Open Graph (OG) image server built in Rust, powering dynamic social card generation for UmamiQR — a free QR code digital menu system for restaurants.
Generates 1200×630px WebP images on the fly via query parameters, with in-memory caching and an embedded font — zero runtime file I/O.
- Dynamic OG image generation from URL query parameters
- In-memory LRU cache (Moka) — up to 500 images with 24h TTL
- CPU-bound rendering offloaded to Tokio's blocking thread pool
- Embedded font (Fraunces) — compiled into the binary, no file system dependency
- Branded design matching UmamiQR's color palette
- Gzip compression on responses
- Health check endpoint for monitoring
- Rust 2021 edition or later (
rustc 1.75+) cargo
# Development
cargo run
# Production-optimized release
cargo run --releaseThe server starts on 127.0.0.1:3100 by default. Override the port:
PORT=8080 cargo run --releaseGenerates an Open Graph image as a PNG.
| Parameter | Type | Required | Description |
|---|---|---|---|
title |
string |
No | Main headline (max 2 lines). Default: "The Title" |
description |
string |
No | Subtitle/description (max 2 lines). Default: "Free QR Digital Menu for Restaurants" |
section |
string |
No | Optional golden badge label. Omit to hide |
Response:
Content-Type: image/webpCache-Control: public, max-age=86400Content-Encoding: gzip(if client accepts compression)
# Basic
curl "http://127.0.0.1:3100/og?title=Hello%20World&description=My%20awesome%20project" -o og.webp
# With section badge
curl "http://127.0.0.1:3100/og?title=Digital%20Restaurant%20Menu&description=Free%20QR%20code%20menu%20system§ion=About" -o og.webp
# Save and set as meta tag
<meta property="og:image" content="https://your-domain.com/og?title=Your%20Title&description=Your%20Description" />Returns 200 OK with body "ok".
┌──────────┐ Query params ┌───────────┐ spawn_blocking ┌──────────┐
│ Client │ ──────────────► │ Axum │ ──────────────────► │ Takumi │
│ (Browser)│ ◄────────────── │ Server │ ◄────────────────── │ (Render) │
└──────────┘ WebP + gzip └─────┬─────┘ └──────────┘
│
┌────────▼────────┐
│ Moka Cache │
│ (500 entries) │
└─────────────────┘
| Component | Tech |
|---|---|
| Web framework | Axum 0.8 |
| Async runtime | Tokio |
| OG renderer | Takumi 1.0 |
| Cache | Moka |
| Font | Fraunces (embedded at compile time) |
| Output | 1200×630 WebP (Open Graph standard) |
FROM rust:1-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/og-server /usr/local/bin/
EXPOSE 3100
CMD ["og-server"]Place behind a reverse proxy that terminates TLS:
server {
listen 443 ssl;
server_name your-domain.com;
location /og {
proxy_pass http://127.0.0.1:3100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}When a URL from your site is shared in AI tools (ChatGPT, Gemini, Claude, Perplexity, Google AI Overviews), the AI fetches the page and reads <meta> tags to generate rich previews with context-aware answers.
User shares link AI fetches page AI reads meta tags
│ │ │
▼ ▼ ▼
"Check this out!" ──► GET /page ──► og:title ──► "This page is about: ..."
response og:image Shows thumbnail
og:description
Add these to your HTML <head>. The values should be server-rendered per page:
<head>
<!-- Core OG tags -->
<meta property="og:title" content="{{ page_title }}" />
<meta property="og:description" content="{{ page_description }}" />
<meta property="og:image" content="https://your-domain.com/og?title={{ url_encoded_title }}&description={{ url_encoded_description }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ canonical_url }}" />
<!-- Twitter Card (same image, different format) -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ page_title }}" />
<meta name="twitter:description" content="{{ page_description }}" />
<meta name="twitter:image" content="https://your-domain.com/og?title={{ url_encoded_title }}&description={{ url_encoded_description }}" />
</head>| Platform | What it reads | Result |
|---|---|---|
| ChatGPT | og:title, og:description, og:image |
Shows page title + thumbnail in shared link preview |
| Google AI Overviews | Full OG + structured data | Cites page content with rich context in AI answers |
| Claude | og:title, og:description, page content |
References page accurately with correct title |
| Gemini | og:image, meta description |
Generates visual card with branded image |
| Perplexity | OG tags + <title> |
Cites source with proper attribution |
| Slack/Discord | og:title, og:image |
Shows link embed with thumbnail |
Laravel (Blade):
<meta property="og:image"
content="https://your-domain.com/og?title={{ urlencode($menu->name) }}&description={{ urlencode('Digital menu — ' . $menu->restaurant->name) }}§ion={{ urlencode($menu->category ?? '') }}"
/>Next.js (App Router):
export async function generateMetadata({ params }: Props) {
const menu = await getMenu(params.id);
const title = menu.name;
const desc = `Digital menu — ${menu.restaurant.name}`;
return {
openGraph: {
title,
description: desc,
images: [
`/og?title=${encodeURIComponent(title)}&description=${encodeURIComponent(desc)}`,
],
},
};
}- Unique titles — each page should have a distinct
og:titleso AI can differentiate content - Descriptive descriptions — include context like restaurant name, cuisine type, or location
- Section badge — use
§ion=for category context (e.g.,section=Appetizers,section=Drinks) - Consistent branding — the OG image uses your brand colors so AI previews stay recognizable
- Fast response — cached responses return in <10ms, so AI crawlers never timeout
MIT