This repo demonstrates how to generate dynamic images using Next + Vercel. It's easy to fork, modify, and deploy. And in most cases the free plan should be sufficient.
Note, you can deploy and use this as a standalone service with the sole purpose of generating images for your mini app.
- Install dependencies:
pnpm install- Run the development server:
pnpm dev- View the demo route:
http://localhost:3000/api/demo
You can pass parameters to customize the template:
http://localhost:3000/api/demo?title=Hello§ion=Guide&description=Your%20custom%20text
- Modify
app/api/demo/route.tsxto customize the image generation or setup new routes by creating by copying thedemodirectory (e.g.app/api/home/route.tsx).
You can create as many different image routes as needed. For example, you might
have one for you home route and then one for different paths or resources that
are part of your application (e.g. app/api/episode/route.tsx for rending an
image representing a particular episode image /api/episodes?id=1).
- Download
.ttffiles from fonts.google.com - Save in
/static/directory - Load in your route:
import { readFileSync } from 'fs';
import path from 'path';
const fontBuffer = readFileSync(path.join(process.cwd(), 'static', 'your-font.ttf'));
// In ImageResponse options:
fonts: [{
name: 'YourFont',
data: fontBuffer,
weight: 400,
style: 'normal',
}]Best for small icons and logos. Inline with data URI:
const imageBuffer = readFileSync(path.join(process.cwd(), 'static', 'icon.png'));
const base64 = `data:image/png;base64,${imageBuffer.toString('base64')}`;For larger images, serve from /public folder:
// Place image in: /public/images/large-image.png
// In development:
<img src="http://localhost:3000/images/large-image.png" />
// In production:
<img src="https://yourdomain.com/images/large-image.png" />
// Or dynamically:
const baseUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000';
<img src={`${baseUrl}/images/large-image.png`} />- Base64: Icons, logos < 10KB (avoids network requests)
- Public folder: Images > 10KB (avoids ~33% base64 size increase)
- External CDN: Large images, multiple variants, or shared across projects
You can use SVGs in two ways:
Define SVG elements directly in your JSX:
const svg = (
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800" fill="none">
{/* SVG paths and elements */}
</svg>
);
// Use it in your component:
<div>{svg}</div>For hosted SVG files:
<img src="https://your-domain.com/your-svg.svg" height={34} width={34} />Cache headers improve performance and reduce costs. Choose based on your content:
If the image at a particular URL will never change:
'Cache-Control': 'public, max-age=31536000, s-maxage=31536000, immutable'Result: Cached for 1 year, never revalidates
Cost: Generate once, serve forever
Example: stats about a completed football match
If the image at a particular URL should periodically change:
'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=86400'Result: Browser 5min → CDN 5m → Stale OK for 1 day
Cost: ~24 generations/day per unique URL
Examples: stats about an in-progress football match
You almost certainly don't want to do this since it will be slow and expensive; and when developing you can use the developer tools in your browser to disable caching to bypass the cache. But for demonstration purposes if you did want to always dynamically generate the image:
'Cache-Control': 'no-cache, no-store, must-revalidate'Result: Always fresh
Cost: Every request generates (expensive and slow!)
Just hope you don't go viral with this setting or you'll be in for an unpleasant bill!
Set cache headers in your ImageResponse:
return new ImageResponse(
(<div>...</div>),
{
width: 1200,
height: 630,
headers: {
'Cache-Control': 'public, max-age=3600, s-maxage=86400, stale-while-revalidate',
},
}
);max-age=3600: Browser cache time of 1 hours-maxage=86400: CDN cache of 24 hoursstale-while-revalidate: Serve stale content while updatingno-cache: Force revalidation on every requestimmutable: Never revalidate (for versioned URLs)
Note: Maximum cache duration is 1 year (31536000 seconds) per HTTP spec.