ipx vs @vercel/og vs satori: Dynamic Image Generation in Node.js (2026)
TL;DR
ipx is the UnJS image optimization server — on-demand image resizing, format conversion, and optimization via URL parameters, powers Nuxt Image. @vercel/og is Vercel's OG image generator — generates Open Graph images from JSX/HTML at the edge using satori under the hood, designed for Next.js. satori is the HTML/CSS-to-SVG converter — converts JSX to SVG using a layout engine, works anywhere (not tied to Vercel), used as the core of @vercel/og. In 2026: ipx for image optimization/CDN, @vercel/og for Next.js OG images, satori for custom HTML-to-image conversion.
Key Takeaways
- ipx: ~1M weekly downloads — UnJS, image optimization server, URL-based transforms
- @vercel/og: ~500K weekly downloads — Vercel, OG image generation, JSX → PNG
- satori: ~1M weekly downloads — Vercel, JSX/HTML → SVG, layout engine, works anywhere
- Different tools: ipx optimizes existing images, satori/og generate new images from HTML
- satori is the engine inside @vercel/og — use satori directly for non-Vercel projects
- ipx is like a self-hosted Cloudinary/imgix
ipx
ipx — image optimization server:
Basic server
import { createIPX, createIPXH3Handler } from "ipx"
import { createApp, toNodeListener } from "h3"
import { createServer } from "node:http"
const ipx = createIPX({
dir: "./public/images", // Source directory
})
const app = createApp()
app.use("/_ipx", createIPXH3Handler(ipx))
createServer(toNodeListener(app)).listen(3000)
// Now access images with transforms via URL:
// http://localhost:3000/_ipx/w_800,h_600,f_webp/photo.jpg
// → Resizes to 800x600 and converts to WebP
URL-based transforms
Transform syntax: /_ipx/{transforms}/{path}
Resize:
/_ipx/w_800/photo.jpg → Width 800px (auto height)
/_ipx/h_600/photo.jpg → Height 600px (auto width)
/_ipx/w_800,h_600/photo.jpg → Exact 800x600
/_ipx/s_200x200/photo.jpg → Square 200x200
Format conversion:
/_ipx/f_webp/photo.jpg → Convert to WebP
/_ipx/f_avif/photo.png → Convert to AVIF
/_ipx/f_png/photo.jpg → Convert to PNG
Quality:
/_ipx/q_80/photo.jpg → 80% quality
/_ipx/w_800,q_75,f_webp/photo.jpg → Resize + quality + format
Fit modes:
/_ipx/fit_cover,w_400,h_400/photo.jpg → Cover (crop)
/_ipx/fit_contain,w_400,h_400/photo.jpg → Contain (letterbox)
/_ipx/fit_fill,w_400,h_400/photo.jpg → Fill (stretch)
Remote images
import { createIPX } from "ipx"
const ipx = createIPX({
// Allow remote image sources:
domains: ["images.unsplash.com", "avatars.githubusercontent.com"],
// Or allow any remote:
fetchOptions: {
headers: { "User-Agent": "IPX/1.0" },
},
})
// /_ipx/w_400,f_webp/https://images.unsplash.com/photo-123
How Nuxt Image uses ipx
// In Nuxt, <NuxtImg> uses ipx for optimization:
// <NuxtImg src="/photo.jpg" width="800" format="webp" />
// Generates: /_ipx/w_800,f_webp/photo.jpg
// nuxt.config.ts:
export default defineNuxtConfig({
image: {
provider: "ipx", // Default — uses built-in ipx
// Or use external providers:
// provider: "cloudinary",
// provider: "imgix",
},
})
@vercel/og
@vercel/og — OG image generation:
Next.js App Router
// app/api/og/route.tsx
import { ImageResponse } from "@vercel/og"
export const runtime = "edge"
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get("title") ?? "PkgPulse"
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0a0a0a",
color: "white",
fontSize: 60,
fontFamily: "Inter",
}}
>
{title}
</div>
),
{
width: 1200,
height: 630,
}
)
}
// Usage: /api/og?title=React%20vs%20Vue
// Returns a 1200x630 PNG image
Dynamic OG images
// app/api/og/route.tsx
import { ImageResponse } from "@vercel/og"
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const pkg1 = searchParams.get("pkg1") ?? "react"
const pkg2 = searchParams.get("pkg2") ?? "vue"
return new ImageResponse(
(
<div style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
color: "white",
padding: 60,
}}>
<div style={{ fontSize: 32, opacity: 0.8, marginBottom: 20 }}>
PkgPulse Comparison
</div>
<div style={{ fontSize: 72, fontWeight: 700 }}>
{pkg1} vs {pkg2}
</div>
<div style={{ fontSize: 28, opacity: 0.7, marginTop: 20 }}>
Compare npm packages side by side
</div>
</div>
),
{ width: 1200, height: 630 },
)
}
Custom fonts
import { ImageResponse } from "@vercel/og"
// Load custom font:
const interBold = fetch(
new URL("../../assets/Inter-Bold.ttf", import.meta.url)
).then((res) => res.arrayBuffer())
export async function GET() {
return new ImageResponse(
<div style={{ fontFamily: "Inter", fontWeight: 700, fontSize: 72 }}>
PkgPulse
</div>,
{
width: 1200,
height: 630,
fonts: [
{ name: "Inter", data: await interBold, weight: 700 },
],
},
)
}
satori
satori — HTML/CSS to SVG:
Basic usage
import satori from "satori"
// Convert JSX to SVG:
const svg = await satori(
<div style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#1a1a1a",
color: "white",
fontSize: 48,
}}>
Hello, World!
</div>,
{
width: 1200,
height: 630,
fonts: [{
name: "Inter",
data: fontBuffer, // ArrayBuffer of font file
weight: 400,
}],
},
)
// svg is a string of SVG markup
console.log(svg)
// → "<svg width="1200" height="630">...</svg>"
Convert SVG to PNG
import satori from "satori"
import sharp from "sharp"
// Generate SVG:
const svg = await satori(
<div style={{ /* ... */ }}>PkgPulse</div>,
{ width: 1200, height: 630, fonts: [/* ... */] },
)
// Convert to PNG with sharp:
const png = await sharp(Buffer.from(svg))
.png()
.toBuffer()
// Or use @resvg/resvg-js for edge environments:
import { Resvg } from "@resvg/resvg-js"
const resvg = new Resvg(svg)
const pngData = resvg.render()
const pngBuffer = pngData.asPng()
Complex layouts
import satori from "satori"
const svg = await satori(
<div style={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
padding: 60,
backgroundColor: "#0f172a",
}}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<img src="https://pkgpulse.com/logo.png" width={48} height={48} />
<span style={{ color: "#94a3b8", fontSize: 24 }}>PkgPulse</span>
</div>
{/* Main content */}
<div style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<h1 style={{ color: "white", fontSize: 64 }}>
React vs Vue
</h1>
</div>
{/* Footer */}
<div style={{ color: "#64748b", fontSize: 20 }}>
Compare npm packages at pkgpulse.com
</div>
</div>,
{ width: 1200, height: 630, fonts: [/* ... */] },
)
Supported CSS
satori supports a subset of CSS (flexbox-based):
Layout:
✅ display: flex
✅ flexDirection, alignItems, justifyContent
✅ gap, padding, margin
✅ width, height, maxWidth, maxHeight
✅ position: absolute/relative
✅ overflow: hidden
Typography:
✅ fontSize, fontWeight, fontFamily
✅ color, textAlign, lineHeight
✅ letterSpacing, textDecoration
✅ textOverflow: ellipsis
Visuals:
✅ backgroundColor, backgroundImage (linear-gradient)
✅ borderRadius, border, borderColor
✅ boxShadow, opacity
✅ <img> tags with src
NOT supported:
❌ display: grid
❌ CSS animations/transitions
❌ :hover, :focus pseudo-classes
❌ media queries
Feature Comparison
| Feature | ipx | @vercel/og | satori |
|---|---|---|---|
| Purpose | Image optimization | OG image generation | HTML → SVG |
| Input | Existing images | JSX/HTML | JSX/HTML |
| Output | Optimized image | PNG image | SVG string |
| Resize/crop | ✅ | ❌ | ❌ |
| Format conversion | ✅ (WebP, AVIF) | PNG only | SVG |
| URL-based API | ✅ | ❌ | ❌ |
| Edge runtime | ❌ (needs sharp) | ✅ | ✅ |
| Custom fonts | N/A | ✅ | ✅ |
| Flexbox layout | N/A | ✅ | ✅ |
| Framework | Any (H3) | Next.js | Any |
| Used by | Nuxt Image | Next.js | @vercel/og |
| Weekly downloads | ~1M | ~500K | ~1M |
When to Use Each
Use ipx if:
- Need on-demand image optimization (resize, format, quality)
- Building a self-hosted image CDN
- Using Nuxt Image or need URL-based transforms
- Want to optimize existing images without a third-party service
Use @vercel/og if:
- Generating OG images in Next.js
- Want the simplest API for dynamic social images
- Deploying on Vercel (edge runtime)
- Need PNG output from JSX templates
Use satori if:
- Need HTML/CSS → SVG conversion outside Next.js/Vercel
- Building custom image generation (badges, certificates, cards)
- Want to use sharp or resvg for final PNG conversion
- Need the most flexibility in image generation
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on ipx v3.x, @vercel/og v0.6.x, and satori v0.10.x.