How to Generate Personalized Open Graph Images with Satori
Generate a share image for every post or profile with one reusable Satori template, route-specific data, explicit fonts, and an Open Graph image URL.
Use one reusable Satori template and pass it the content for the current route. Satori turns a pure JSX-like element tree into SVG; your route can return that SVG or use a framework helper such as Next.js ImageResponse to return an image response. Publish the resulting public URL as the page’s og:image value. A documented common card size is 1200 × 630 pixels, though it is not a universal requirement for every social platform.
The workflow is: resolve the post or profile, render its data with a stable template, return the image from a reachable route, and expose that route in Open Graph metadata. The examples below use Next.js App Router and Satori directly.
1. Choose the rendering route
Use a framework-native metadata image file when the image belongs to a page and its route parameters identify the content. In Next.js App Router, opengraph-image.tsx can read route parameters, fetch the matching post, and return an ImageResponse. Use a custom API route when you need a general-purpose endpoint, such as one that accepts a validated content identifier or title. In either case, make sure the image uses the same content version as the page metadata.
| Approach | Good fit | Check |
|---|---|---|
Next.js opengraph-image.tsx |
One image associated with a route or page | Static optimization and fetch or route-segment settings affect whether output changes with data. |
| Custom API route | A reusable endpoint with explicit inputs or custom response behavior | Validate inputs, set cache behavior deliberately, and return a public URL. |
| Direct Satori output | You want Satori’s SVG output or control over later processing | Satori returns SVG; it does not itself turn the shown result into PNG. |
| Framework image response | You want a framework-supported response from a route | For Next.js, ImageResponse is the documented route pattern. |
2. Install dependencies and prepare a font
For the Next.js example, install the framework’s OG helper and load a font file that Satori supports. The Satori README lists TTF, OTF, and WOFF, and says WOFF2 is not supported. Check that the font license allows your intended server-side use.
npm install next
Put a compatible font file somewhere your route can read it, such as app/fonts/Inter-Regular.ttf. Load the bytes once at module scope so repeated renders can reuse them. Node.js examples can read a file with fs.readFile; in an environment where filesystem access is unavailable, load the bytes using that runtime’s supported asset mechanism.
import { readFile } from 'node:fs/promises'
const inter = await readFile(new URL('./fonts/Inter-Regular.ttf', import.meta.url))
3. Build a reusable Satori template
Satori accepts JSX or a React-like element object, but it is not a full browser. Keep the tree pure and stateless, use supported elements and inline styles, and use Flexbox as the main layout model. The minimal core call below returns an SVG string. The font bytes are supplied explicitly.
import satori from 'satori'
export async function renderOgSvg(title: string, description: string) {
return satori(
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
width: '100%',
height: '100%',
padding: 72,
background: '#101828',
color: '#ffffff',
fontFamily: 'Inter',
}}
>
<div style={{ display: 'flex', fontSize: 26, color: '#b9c5d6' }}>
Acme Journal
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ display: 'flex', fontSize: 64, fontWeight: 700, lineHeight: 1.1 }}>
{title}
</div>
<div style={{ display: 'flex', fontSize: 28, color: '#d2d9e5' }}>
{description}
</div>
</div>
<div style={{ display: 'flex', fontSize: 22, color: '#b9c5d6' }}>
acme.example
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{ name: 'Inter', data: inter, weight: 400, style: 'normal' },
],
},
)
}
This is a template illustration, not a promise that every CSS property behaves as it would in a browser. Keep layout styles within Satori’s supported subset and inspect the rendered result. Consider a shorter title fallback or a deliberate title limit so unusually long content does not crowd out other card elements.
4. Generate a route-specific image in Next.js
Create app/blog/[slug]/opengraph-image.tsx. Fetch the content for the route, handle missing records, and pass the resolved title and description to the template. This example uses ImageResponse, the Next.js route pattern for returning a generated image.
import { ImageResponse } from 'next/og'
import { readFile } from 'node:fs/promises'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
const inter = await readFile(new URL('../../../fonts/Inter-Regular.ttf', import.meta.url))
async function getPost(slug: string) {
// Replace with your database or CMS lookup.
const posts: Record<string, { title: string; description: string }> = {
'satori-guide': {
title: 'Generate Open Graph images with Satori',
description: 'A reusable template for personalized share cards.',
},
}
return posts[slug] ?? null
}
export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
const title = post?.title ?? 'Article not found'
const description = post?.description ?? 'Explore the latest from Acme Journal.'
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
width: '100%',
height: '100%',
padding: 72,
background: '#101828',
color: '#ffffff',
fontFamily: 'Inter',
}}
>
<div style={{ display: 'flex', fontSize: 26, color: '#b9c5d6' }}>Acme Journal</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div style={{ display: 'flex', fontSize: 64, fontWeight: 700, lineHeight: 1.1 }}>{title}</div>
<div style={{ display: 'flex', fontSize: 28, color: '#d2d9e5' }}>{description}</div>
</div>
<div style={{ display: 'flex', fontSize: 22, color: '#b9c5d6' }}>acme.example</div>
</div>
),
{
...size,
fonts: [{ name: 'Inter', data: inter, weight: 400, style: 'normal' }],
},
)
}
Route parameter typing varies with the Next.js version; use the parameter shape required by your installed version. The metadata image convention documents a route parameter and image response pattern. If your data source is remote, fetch the content on this path and configure static or request-time behavior to match your update needs. A missing slug should have an intentional fallback or a not-found response rather than producing an unhandled rendering error.
5. Add the generated URL to Open Graph metadata
For a page with a metadata image file, Next.js can emit the image metadata for you. For a custom route or a different framework, set an absolute, publicly reachable URL in the page metadata:
<meta property="og:image" content="https://example.com/blog/satori-guide/opengraph-image" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="A share card for the Satori guide" />
Next.js metadata files can provide image type, dimensions, and alt metadata through their exported values. Its metadata example uses 1200 × 630; Vercel also documents that as the default ImageResponse size. Treat those as documented conventions, then verify the requirements of the platforms where you share links. Deploy the route before referencing it: social crawlers need to reach the image URL without an interactive login or browser-only state.
6. Personalize beyond a blog title
The same template can render a profile, product, event, or other page. Resolve only the fields needed for the card and pass them as data. A profile card might use a display name and avatar; a product card might use a name and category. For dynamic text, provide safe fallbacks, and for user-controlled values, avoid treating the content as markup or executable code.
- Long text: impose a product-appropriate length limit, wrap it, or reduce font size within a deliberate range. Test the longest realistic title.
- Optional fields: omit the row or substitute a neutral fallback instead of rendering
nullor an empty label. - Avatars and images: use an image element with explicit width and height. Satori documents that background images stretch by default when no size is set; size image assets deliberately.
- Locale: pass
langwhere appropriate. Satori can dynamically load missing emoji or font assets, but complex scripts need attention. - RTL and mixed direction text: Satori uses HarfBuzz for complex script shaping such as Arabic, but does not support full Unicode bidirectional layout. Inspect mixed right-to-left and left-to-right content directly.
7. Fonts, assets, and runtime options
| Concern | Implementation note |
|---|---|
| Font bytes | Pass Node.js Buffer or web ArrayBuffer data, with family name, weight, and style. Supported formats listed in the README are TTF, OTF, and WOFF; WOFF2 is not supported there. |
| Font reuse | If fonts do not change, keep font objects or loaded bytes reusable across renders to avoid reloading them for every image. |
| SVG text | Glyphs are converted to SVG paths by default. embedFont: false switches to <text>; use that only if the downstream SVG environment can supply suitable fonts. |
| Elements and styles | Satori supports a subset of HTML and CSS, with Flexbox as its primary layout model. Do not assume browser-only CSS, state, or effects work. |
| Runtime | The README lists browsers, Node.js 16+, and Web Workers. Its standalone build for constrained WASM environments such as Cloudflare Workers requires supplying and initializing Yoga’s yoga.wasm. |
| Output | Satori returns SVG. Use a framework image response such as Next.js ImageResponse when your route should return a raster image response. |
8. Caching, performance, and reliability
There is no performance benchmark in the cited documentation, so measure generation on your deployment and with your actual templates and assets. Reuse unchanged font data, keep the tree and data lookup small, and avoid repeatedly fetching the same content or assets during a render. If your card changes only when its underlying post changes, caching can reduce repeated work, but freshness depends on your framework and host configuration.
Next.js says metadata image output is statically optimized by default and that fetch or route-segment options can change behavior. Vercel says @vercel/og adds CDN headers to cache computed images. Those statements do not establish a universal cache lifetime or invalidation policy. Decide how edits, publishing, and deletion should affect the image URL, then verify the deployed response and cache behavior. A stable route can serve stale content if its cache is not revalidated; a request-time route can add content-fetch work on each miss.
- Use a deterministic template and explicit dimensions.
- Provide a fallback for missing data and a deliberate response for deleted or unpublished content.
- Keep fonts and static assets available in the production runtime, not only in local development.
- Check the endpoint from outside your logged-in browser and confirm the response is an image with the expected dimensions.
- Keep the page title and OG image data aligned so crawlers do not see mismatched versions.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Unsupported element or style error | The tree uses browser HTML/CSS or a layout feature outside Satori’s supported subset. | Reduce the tree to supported elements and inline styles; prefer Flexbox and inspect the Satori README’s supported features. |
| Font is missing or the wrong face appears | Font bytes were not loaded, the family name does not match, or the format is unsupported. | Pass valid TTF, OTF, or WOFF bytes and matching family, weight, and style. Do not use WOFF2 according to the current README. |
| Text clips or overlaps | Dynamic content is longer than the designed card, or the layout assumes browser wrapping behavior. | Test long and short values, constrain text, adjust supported Flexbox layout, and inspect the actual output. |
| Emoji or non-Latin text renders unexpectedly | Required glyph assets may not be available, or shaping and bidirectional support may not match the language mix. | Set language where appropriate, supply suitable font data, and directly inspect complex scripts and mixed RTL/LTR output. |
| Image asset is stretched or absent | Asset URL or runtime access is unavailable, or a background has no explicit size. | Use a reachable asset, set explicit dimensions, and verify the production runtime can fetch or load it. |
| Direct SVG looks different downstream | SVG text, path handling, or downstream fonts differ from expectations. | Remember Satori outlines glyphs by default. If using embedFont: false, confirm the consumer supplies a suitable font. |
| OG image is old after editing a post | The route or host is serving cached or statically optimized output. | Review Next.js fetch and route-segment settings plus host CDN behavior; configure revalidation or request-time rendering to fit your freshness needs. |
| Social preview has no image | The URL may be relative, private, unreachable, or returning an error to the crawler. | Use an absolute public URL, deploy the route, check the response and content type, and confirm the page exposes og:image. |
| Works locally but fails on a worker | The runtime may need a standalone Satori build and Yoga WASM initialization. | Follow the README’s standalone setup for the constrained runtime and ensure yoga.wasm is supplied and initialized. |
10. A quick implementation checklist
- Resolve the current route’s content from a trusted source.
- Render it with a pure, reusable Satori element tree.
- Choose and load a supported font format, with the intended weight and style.
- Set explicit dimensions; use 1200 × 630 as a documented common example where it fits.
- Handle missing fields, long titles, image assets, locale, and complex scripts.
- Return the SVG or use a framework image response for the route.
- Publish an absolute public URL as
og:imageand include dimensions and alt text where applicable. - Set and verify cache behavior for your framework and host.
- Inspect the deployed output with representative content, including edge cases.
Or skip the browser setup
Satori generates a designed card from data; ScreenshotNeo captures a rendered web page as an image or PDF. If the page you want to share already exists and you need a screenshot, a single request can capture it. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, and cache hits are not billed. Its MCP server lets AI agents take screenshots, and the free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. Learn more at ScreenshotNeo, or sign up free for 1,000 screenshots a month with no card.
FAQ
Does Satori create a PNG by itself?
The Satori call shown here returns SVG. A framework helper such as Next.js ImageResponse provides an image response route.
Can I use a different size from 1200 × 630?
Yes. The sources document 1200 × 630 as a common example and default response size, not a universal platform rule. Choose dimensions for your use case and confirm target platform requirements.
Can each user have a unique profile card?
Yes. Resolve the user’s public profile fields from the route or validated input and pass them to the same template. Add a fallback for unavailable or private profile data.
Does the image update immediately when content changes?
That depends on static optimization, fetch settings, and host caching. Check the deployed configuration and choose an invalidation or revalidation approach that matches your publishing workflow.


