How to Generate Dynamic OG Images in a Next.js App
A link to your app goes out on Slack, Twitter, or LinkedIn, and the preview card that shows up is either your homepage's og:image, cropped wrong, or nothing at all. Every page past the homepage shares the same static picture, so a pricing page, a blog post, and a signup page all look identical in a feed where the image is the first thing anyone sees. That flat click-through rate on shared links is not a copywriting problem, it is a missing route.
An Open Graph image is the picture a browser or app pulls from a page's og:image meta tag and shows in a link preview, and generating one per page instead of one per site is what makes a shared link worth clicking. Next.js can build that image on the fly, per request, with the same JSX you already write for the page itself. This walks through the route that does it, where the CSS support runs out, and the point at which hand-building every template stops being a good use of an afternoon.
What Is an Open Graph Image, and Why Does It Need Its Own Route?
An Open Graph image is a plain PNG or JPEG referenced by a page's og:image tag, defined by the Open Graph protocol that every major platform now reads before rendering a link preview. A static image works for a single-page site. A multi-page app needs one image per URL, a blog post's title, a product's price, a doc page's heading, or every share looks like the same generic homepage screenshot.
Hand-exporting a PNG per page in a design tool does not scale past a handful of pages, and it goes stale the moment a title changes. The fix is generating the image at request time from the page's own data, so the preview is never out of sync with the content.
How Does @vercel/og Turn a JSX Component Into a PNG?
@vercel/og is a library that renders a JSX component to an image inside a Next.js route handler, using a rendering engine called Satori to convert the markup into pixels. You write the layout the same way you'd write any component, divs, flexbox, text, and the route returns image bytes instead of HTML.
@vercel/og renders images at 1200 by 630 pixels, the standard size Facebook, LinkedIn, and Twitter/X expect for a link preview. Call the route with query parameters, a title, a description, a template name, and it returns a fresh image for those exact values.
Because the image comes back from an ordinary route handler, standard Next.js caching applies. Set a Cache-Control header on the response, or let the platform's edge cache hold it, and the same title-and-description combination doesn't re-render on every crawler hit. That matters more than it sounds: a post shared a thousand times in an hour would otherwise trigger a thousand separate renders instead of one cached PNG served a thousand times.
How Do You Build Your First OG Image Route?
The route itself is a handful of steps, whether you're rendering one fixed layout or the first of several templates.
- Create a route handler at app/api/og/route.tsx, or a dynamic app/api/og/[template]/route.tsx if you plan to support more than one layout.
- Import ImageResponse from next/og and read the title and description from the request's search params.
- Return ImageResponse with a flexbox div built from plain JSX, sized to width 1200 and height 630.
- Point the page's og:image meta tag at that route, passing the page's own title and description as query parameters.
- Load a font file directly, since Satori needs the actual font bytes rather than a linked stylesheet, if the design needs anything beyond the system default.
- Paste the finished URL into a link-preview debugger to confirm the crawler sees the rendered image, not a 500.
What Are the Limits of Satori's CSS Subset?
Satori, the rendering engine behind @vercel/og, supports only a subset of CSS: flexbox layouts, no CSS grid, and no floats. Every container needs an explicit display: flex if it holds more than one child, because Satori's default block layout doesn't wrap the way a browser does.
Fonts are the other trap. Satori needs the actual font file bytes passed in at render time, not a linked stylesheet or @font-face rule, so a font that renders fine in the browser can come back blank or boxed in the generated image until you load its file directly. Budget time for two things the first pass: relearning layout in flexbox-only CSS, and tracking down font files in the right format.
Long titles are the other place this bites. A CSS truncation trick like text-overflow: ellipsis behaves differently inside Satori's layout engine than in a browser, so a title field needs an explicit max character count or a manual line-clamp built from flex-wrap rather than relying on the browser's usual text-overflow handling. Test with the longest real title your content model allows, not a short placeholder, before shipping the route.
Should You Hand-Build Every Template or Start From a Boilerplate?
One template for one route is a short afternoon. A real app usually wants several, a blog post layout, a product layout, a docs layout, a default fallback, and each one repeats the same font-loading and flexbox debugging from scratch. That is the point where BuyCoded's OG Image Generator Boilerplate earns its price: a Next.js 15 boilerplate with 50 templates already built, wired to a route that takes a title and description and returns the finished PNG, plus a /preview page where you pick a template from a dropdown, fill in its fields, and see the rendered image before wiring it into a real page.
| Task | Building it yourself | OG Image Generator Boilerplate |
|---|---|---|
| First working route | An afternoon, once fonts and flexbox are debugged | Already wired, point og:image at the API path |
| Additional templates | Repeat the same setup per layout | Drop a component in templates/, register it, about 10 minutes |
| Font handling | Track down font files and Satori's loading quirks yourself | DejaVu Sans Regular and Bold bundled, broad Unicode coverage |
| Live QA | Curl the route and check the PNG by hand | /preview page renders every template with a live form |
| Deploy target | Whatever your app already ships to | Same, runtime nodejs works on Vercel, Render, Railway, Fly, or a VPS |
Is It Worth Paying for a Starter Kit Instead of Building From Scratch?
The honest objection is that @vercel/og is a free library, and a boilerplate on top of it costs money for something you could technically write yourself. That holds for one template. It holds less once the count reaches five or six layouts across a real site, each needing the same flexbox and font debugging that only has to happen once, and the boilerplate is a one-time purchase with a commercial license covering client and agency work, not a subscription, refundable within 14 days if it doesn't do what the product page says.
The other objection is lock-in: what if the templates don't match your brand? Each template is a plain React component with a defineTemplate declaration of its own fields and defaults, so editing colors, fonts, or layout is normal component work, not a proprietary format you're stuck with. Nothing about the route or the templates is tied to BuyCoded's hosting or account system once you own the source.
Add a Unique Preview Image to Every Page
Start with one route and one template, confirm it renders at 1200 by 630 in a link-preview debugger, then decide whether 50 pre-built layouts are worth skipping the rest of the templates by hand. Browse the full catalog at BuyCoded's product list to see what else ships the same way: instant download, commercial license, 14-day refund.
Frequently asked questions
What size should an Open Graph image be?
1200 by 630 pixels is the standard Open Graph image size, matching what Facebook, LinkedIn, and Twitter/X expect for a link preview card. Going smaller risks a blurry crop; going much larger just adds load time without improving how the crawler renders it. @vercel/og and most boilerplates, including BuyCoded's OG Image Generator Boilerplate, default to this exact size.
Does @vercel/og work outside of Vercel's own hosting?
Yes. The route runs with runtime nodejs, so a boilerplate built around it deploys the same way on Render, Railway, Fly, a Docker container, or a plain VPS as it does on Vercel. Vercel is simply the easiest target because ImageResponse was built with it in mind, not a requirement for it to work.
Why does my custom font not show up in the generated image?
Satori needs the actual font file bytes passed in at render time, not a linked stylesheet or @font-face rule, so a font that displays fine in the browser can render blank or boxed in the image. Load the font file directly in the route and pass it into ImageResponse's fonts option to fix it.
Can I use a paid OG image boilerplate for client projects?
That depends on the specific license, so check it before building on top of one. BuyCoded's OG Image Generator Boilerplate license covers unlimited personal and client or agency use, so it can ship inside a paid client project under your own name, with the one restriction being you can't resell the boilerplate or its templates as a competing starter pack.
How long does it take to add a new OG image template?
For a boilerplate built around a defineTemplate pattern, adding a new layout means dropping a React component into a templates folder and registering it in an index file, roughly 10 minutes once you've done it once. Building the same layout from a blank @vercel/og route, including font and flexbox debugging, usually takes closer to an afternoon.
What happens if the OG image boilerplate doesn't fit my project?
BuyCoded backs every product, including the OG Image Generator Boilerplate, with a 14-day refund if it doesn't work as described on the product page. Emailing hello@buycoded.com within that window gets a refund rather than leaving you stuck with code you can't use.