Examples
Open Graph image
1200×630 social-share images at single-digit-millisecond render time.

OG images are Sone's natural domain — fixed-size, programmatic, generated per request. Here's a typical blog-post OG image.
import { Column, Photo, Row, Span, Text, sone } from "sone";
function OgImage({ title, author, readTime }: {
title: string;
author: string;
readTime: number;
}) {
return Column(
// Top: brand
Row(
Photo("./logo.png").width(36).height(36),
Text("acme.dev").size(16).color("#999").weight("bold"),
).gap(12).alignItems("center"),
// Middle: title (autofit + balanced wrap = always looks good)
Text(title)
.size(64)
.weight("bold")
.lineHeight(1.1)
.color("white")
.textWrap("balance")
.flex(1),
// Bottom: byline
Row(
Photo("./avatar.jpg").width(40).height(40).rounded(20),
Column(
Text(author).size(15).weight("bold").color("white"),
Text(`${readTime} min read`).size(13).color("#999"),
).gap(2),
).gap(12).alignItems("center"),
)
.width(1200).height(630)
.padding(64)
.gap(24)
.bg("black")
.justifyContent("space-between");
}
const buffer = await sone(
OgImage({
title: "How we cut PDF render time from 2 seconds to 30 milliseconds",
author: "Alex Chen",
readTime: 8,
}),
).jpg(0.92);Notes
- Balanced wrap on the title means short and long titles both look polished — no awkwardly short last lines.
- Use
.jpg(0.92)for OG images. Most social platforms re-encode anyway; quality 0.92 keeps file size under 200KB. - Caching — for high-traffic blogs, cache the rendered buffer. The same title always produces the same bytes.
In a Next.js / Hono / Express handler
// Next.js — app/og/route.ts
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const title = searchParams.get("title") ?? "Untitled";
const buffer = await sone(OgImage({ title, author: "—", readTime: 1 })).jpg(0.92);
return new Response(buffer, { headers: { "Content-Type": "image/jpeg" } });
}Single-digit millisecond render means you can afford to render on every request — no need for build-time pre-rendering.