Boost Site Engagement with Dynamic Open Graph Images in Next.js

- Open Graph images improve how links appear when shared. They can increase attention and click-through rate, but they are not a direct Google ranking factor.
- In a modern Next.js App Router project, the simplest implementation is an
opengraph-image.tsx metadata route using ImageResponse from next/og.- Add
twitter-image.tsx when we want an explicit image for X cards; it can reuse the same renderer.- Use a 1200 × 630 canvas for a broadly compatible landscape preview, and keep important text away from the edges.
- Social crawlers need a public, absolute, HTTPS image URL. They cannot use
localhost, authentication-protected routes, or browser-only JavaScript.- Keep image inputs bounded and predictable. A stable post slug is safer than accepting arbitrary titles, colours, and image URLs through query parameters.
- Social platforms cache link previews independently. Changing the generated image does not guarantee that an existing preview will refresh immediately.
A link rarely travels alone. When someone shares an article in LinkedIn, Slack, WhatsApp, Facebook, Discord, or X, the receiving platform usually turns it into a preview containing a title, description, and image.
That image carries more weight than we often give it credit for. A generic logo tells readers very little. A page-specific image containing the article title, category, and brand gives them a reason to pause and understand what the link contains.
Creating a separate image manually for every article or product page is possible, but it becomes tedious as a site grows. Dynamic Open Graph images solve that operational problem: we design one template, feed it page data, and let Next.js generate the correct image for every URL.
This guide shows the current App Router approach using next/og, explains what changed from older @vercel/og tutorials, and covers the details that tend to cause trouble in production.
What Is an Open Graph Image?
The Open Graph protocol describes how a web page should appear when another platform turns it into a rich object. Its core properties include:
og:titleog:typeog:imageog:url
Optional properties such as og:description, og:site_name, og:image:width, og:image:height, and og:image:alt give crawlers more context.
An Open Graph image is simply the public image referenced by og:image. What makes it dynamic is that the image response is generated from the current page's data rather than selected from a manually maintained folder.
For example, every blog post can use the same visual system while changing the following elements:
- Article title
- Category or content type
- Author or publication date
- Brand colour
- Product image or illustration
- Customer, project, or event name
The result is consistent branding without forcing an editor to create a new social asset for every page.
Do Dynamic OG Images Improve SEO?
Not directly. Adding a dynamic image does not make a page rank higher simply because the image was generated with Next.js.
The value appears after discovery. A clear, relevant preview can make a shared link more noticeable, communicate the page's purpose before the click, and create a consistent brand experience across social feeds and messaging apps. That can improve distribution and referral traffic.
Dynamic images also improve publishing operations. If a site has thousands of articles, products, job listings, or public profiles, one maintained template is far easier to keep accurate than thousands of static design files.
We should therefore treat OG images as a content distribution and presentation layer, not as a shortcut to search rankings.
The Modern Next.js Approach
Older guides often install @vercel/og, create a pages/api/og-image.js endpoint, add runtime: 'edge', and manually write <meta> tags with next/head.
That pattern can still be relevant to older Pages Router applications, but it is no longer the best starting point for a new App Router project.
Next.js now provides:
ImageResponsethroughnext/og- File-based
opengraph-imageandtwitter-imagemetadata routes - The
metadataobject andgenerateMetadatafunction - Automatic generation of the corresponding image metadata tags
generateImageMetadatawhen a route needs dynamic alt text or multiple image variants
Another important correction is runtime support. OG generation is no longer restricted to the Edge runtime. Current Next.js and Vercel documentation support the Node.js runtime, which is the default in modern Next.js route segments. That also makes local font and image loading easier when Node.js APIs are needed.
Project Structure
For a blog with dynamic routes, we can keep the image generator beside the page it represents:
app/
├── layout.tsx
└── blogs/
└── [slug]/
├── page.tsx
├── opengraph-image.tsx
└── twitter-image.tsxWhen a crawler visits /blogs/dynamic-og-images, Next.js can expose a generated image for that exact route and add it to the page metadata.
Step 1: Configure Site-Wide Metadata
Start by defining a production origin with metadataBase. Next.js can then resolve relative canonical and image URLs into absolute URLs.
// app/layout.tsx
import type { Metadata } from 'next'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.example.com'
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: {
default: 'F22 Labs',
template: '%s | F22 Labs',
},
description: 'Engineering insights for building dependable digital products.',
openGraph: {
siteName: 'F22 Labs',
locale: 'en_US',
type: 'website',
},
}
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}The fallback URL helps local builds, but production should always provide the correct NEXT_PUBLIC_SITE_URL. A wrong metadataBase can quietly produce previews that point to the wrong domain.
Step 2: Generate Metadata for Each Article
The page metadata should come from the same record used to render the article. This prevents the HTML title, description, canonical URL, and OG image from drifting apart.
// app/blogs/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/posts'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) {
return { title: 'Article not found' }
}
return {
title: post.title,
description: post.description,
alternates: {
canonical: `/blogs/${slug}`,
},
openGraph: {
title: post.title,
description: post.description,
type: 'article',
url: `/blogs/${slug}`,
publishedTime: post.publishedAt,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.description,
},
}
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<p>{post.description}</p>
{/* Render the article body here. */}
</article>
)
}The file-based image routes in the next steps supply the actual Open Graph and X card images, so we do not have to construct their URLs manually inside generateMetadata.
Let’s Build Fast, Scalable Web Apps with Next.js
Partner with F22 Labs to build high-performance Next.js apps that load instantly, scale easily, and deliver seamless user experiences.
Step 3: Create the Dynamic Open Graph Image
Create opengraph-image.tsx in the same route segment as the page:
// app/blogs/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPostBySlug } from '@/lib/posts'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
type Props = {
params: Promise<{ slug: string }>
}
function shorten(text: string, limit = 92) {
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text
}
export async function generateImageMetadata({ params }: Props) {
const { slug } = await params
const post = await getPostBySlug(slug)
return [
{
id: 'default',
alt: post ? `${post.title} | F22 Labs` : 'F22 Labs article preview',
size,
contentType,
},
]
}
export default async function OpenGraphImage({ params }: Props) {
const { slug } = await params
const post = await getPostBySlug(slug)
const title = shorten(post?.title ?? 'Engineering ideas worth sharing')
const category = post?.category ?? 'F22 Labs Insights'
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
background: 'linear-gradient(135deg, #111827 0%, #1f2937 55%, #7f1d1d 100%)',
color: '#ffffff',
padding: '72px 80px',
}}
>
<div
style={{
display: 'flex',
fontSize: 28,
fontWeight: 700,
color: '#fca5a5',
}}
>
{category}
</div>
<div
style={{
display: 'flex',
maxWidth: 1040,
fontSize: title.length > 68 ? 58 : 68,
lineHeight: 1.08,
fontWeight: 800,
letterSpacing: '-0.03em',
}}
>
{title}
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: 26,
}}
>
<div style={{ display: 'flex', fontWeight: 700 }}>F22 Labs</div>
<div style={{ display: 'flex', color: '#d1d5db' }}>f22labs.com</div>
</div>
</div>
),
size,
)
}ImageResponse converts the React and CSS template into an image response. Under the hood, the renderer supports a useful but limited subset of CSS. Flexbox and absolute positioning work well; CSS Grid does not.
The title is deliberately bounded. Without a character limit or font-size adjustment, an unexpectedly long CMS title can overflow the canvas or push branding outside the visible area.
Step 4: Reuse the Image for X Cards
If the same 1200 × 630 design works for both Open Graph and X, we can reuse the implementation:
// app/blogs/[slug]/twitter-image.tsx
export {
contentType,
default,
generateImageMetadata,
size,
} from './opengraph-image'This keeps the two previews visually consistent without maintaining duplicate templates. If X needs different copy or dimensions, create a separate twitter-image.tsx renderer instead.
Adding a Custom Font
System fonts keep the first implementation small and dependable. When brand typography matters, place a font file beside the metadata route and load it once:
const interSemiBold = fetch(
new URL('./Inter-SemiBold.ttf', import.meta.url),
).then((response) => response.arrayBuffer())
export default async function OpenGraphImage({ params }: Props) {
const fontData = await interSemiBold
return new ImageResponse(
<div
style={{
display: 'flex',
width: '100%',
height: '100%',
fontFamily: 'Inter',
}}
>
{/* Template content */}
</div>,
{
...size,
fonts: [
{
name: 'Inter',
data: fontData,
style: 'normal',
weight: 600,
},
],
},
)
}Use WOFF or TTF files and include only the weights the template needs. According to the current Next.js documentation, the image route's bundled assets must stay within a 500 KB limit, so several heavy font files can break an otherwise simple design.
When a Shared Route Handler Makes More Sense
File-based metadata routes are ideal when the image belongs naturally to a route segment. A shared Route Handler can be useful when several page types use one image service, when another application also needs the renderer, or when we need explicit response headers.
Even then, the safest public input is usually a stable identifier:
/api/og?type=article&slug=dynamic-open-graph-imagesThe server can validate type, look up the title and category using slug, and render trusted data. Accepting an arbitrary title, background colour, remote image URL, and font URL from anyone creates unnecessary problems:
- Unbounded cache entries
- Unexpected rendering cost
- Layout abuse through oversized input
- Server-side requests to untrusted resources
- Accidental exposure of private or user-specific information
If query parameters are unavoidable, allowlist values, cap their lengths, validate remote hosts, and return a consistent fallback for invalid requests.
Caching Dynamic OG Images
OG images are excellent cache candidates because many people may share the same page while its title changes infrequently.
On Vercel, generated OG images can be cached on the CDN. In other environments, behaviour depends on the adapter and deployment platform, so we should inspect the actual response headers rather than assume the cache is working.
A useful strategy is:
- Build the cache key from stable public data, such as the article slug and content version.
- Cache successful image responses at the CDN.
- Revalidate or change the version only when visible image content changes.
- Avoid random cache-busting query parameters on every request.
The page's broader Next.js rendering strategy also matters. Content that changes every request may force image regeneration, while published articles usually benefit from stable, cacheable output.
Why an old preview can remain visible
There are two separate caches:
- Our application or CDN caches the generated image.
- LinkedIn, Facebook, Slack, X, and other consumers may cache the page metadata and downloaded image independently.
Purging our CDN does not necessarily purge a social platform's cache. LinkedIn's Post Inspector and Meta's Sharing Debugger can request a fresh crawl, but already-published posts may keep their original preview.
Design Rules That Survive Different Platforms
The same image can be cropped, resized, compressed, or displayed beside different amounts of text. A resilient template needs breathing room.
Use a broadly compatible canvas
Meta recommends 1200 × 630 pixels for high-resolution link previews. The 1.91:1 ratio also works well across many other platforms, although no single design is guaranteed to render identically everywhere.
Keep a safe area
Place the headline, logo, and category away from all four edges. Do not rely on a small footer being visible after cropping.
Design for a phone-sized preview
The source image may be 1200 pixels wide, but readers often see it as a small card. Large type, short copy, strong contrast, and one clear focal point matter more than decorative detail.
Build for real content extremes
Test:
- A 20-character title
- A 90-character title
- Long unbroken words
- Ampersands, apostrophes, and emoji
- Missing categories and images
- Right-to-left or multilingual text, when supported by the product
Add meaningful alt text
Open Graph supports og:image:alt. Describe the image's purpose instead of repeating generic text such as “social image.” Dynamic alt text can include the article title and site name.
Let’s Build Fast, Scalable Web Apps with Next.js
Partner with F22 Labs to build high-performance Next.js apps that load instantly, scale easily, and deliver seamless user experiences.
Do not put private data in a public preview
Social crawlers must be able to reach the image without a user session. That means generated previews should never contain private dashboards, account balances, personal schedules, unpublished documents, or permission-dependent data.
Performance and Reliability Checklist
- Keep the renderer deterministic: the same content version should produce the same image.
- Load CMS data once and reuse a cached data-access function where appropriate.
- Keep fonts, logos, and background assets small.
- Prefer local or allowlisted assets over arbitrary remote URLs.
- Set timeouts for external asset requests and provide a fallback design.
- Limit title and description lengths before rendering.
- Return a valid fallback image when content is missing.
- Monitor image response latency, errors, and cache hit rate.
- Verify that production URLs use HTTPS and return
200 OKwithout authentication. - Check the final page source to confirm that the expected
og:imageandtwitter:imagetags are server-rendered.
Common Problems and Their Fixes
| Problem | Likely cause | Fix |
| No image appears in the preview | Image URL is relative, private, broken, or unavailable to the crawler | Configure metadataBase, use HTTPS, and test the exact public image URL |
| The old image keeps appearing | The social platform cached an earlier crawl | Use the platform's inspector or debugger and remember that existing posts may not update |
| Text is clipped | Title length and font size are fixed | Truncate carefully, adjust type size by length, and keep a safe area |
| Build fails after adding fonts | Metadata image bundle exceeds the asset limit | Subset the font and include only required weights |
| CSS looks different from the browser | ImageResponse does not implement the complete browser CSS engine | Use supported CSS, especially Flexbox, and simplify the layout |
| Image works locally but not in production | Environment-based origin is missing or incorrect | Set and validate the production site URL during deployment |
| Every request regenerates the image | Dynamic inputs or cache configuration prevent reuse | Use stable identifiers and verify deployed cache headers |
| Scrapers trigger excessive work | Public query parameters create unlimited render combinations | Validate and bound inputs, or generate from server-side records by slug |
How to Test the Implementation
Do not stop after opening the image route in a browser. Test the complete sharing path.
- Open the deployed page and confirm that it returns
200 OK. - View the rendered HTML source and find
og:title,og:image,og:url,twitter:card, andtwitter:image. - Open the exact image URL in a private browser window.
- Check its content type, dimensions, and response time.
- Test a short title, long title, missing post, and non-Latin title if the site supports one.
- Run the URL through Meta's Sharing Debugger and LinkedIn's Post Inspector.
- Share it in a private Slack or Discord channel and on a test mobile chat.
- Update the article title and confirm that the application cache and social cache behave as expected.
Testing several real platforms matters because each crawler makes its own decisions about cropping, caching, and fallback metadata.
What About Pages Router Applications?
A Pages Router application can still generate an image from an API route and place its absolute URL in next/head. In that setup, @vercel/og may remain appropriate.
We should not, however, copy two assumptions from older tutorials into a current implementation:
- OG generation is not inherently limited to the Edge runtime.
- The image URL should not interpolate raw title and description values without
encodeURIComponent, validation, and length limits.
If the application is already moving toward App Router, metadata files and next/og provide the cleaner long-term path.
Frequently Asked Questions
What size should a Next.js Open Graph image be?
Use 1200 × 630 pixels as a broadly compatible landscape default. Keep important text and logos inside a generous safe area because social platforms may resize or crop previews differently across feeds and devices.
Do dynamic Open Graph images directly improve Google rankings?
No. They primarily improve how links appear when shared. Better previews can support click-through rate, referral traffic, recognition, and content distribution, but generating an OG image does not create a direct ranking boost.
Should we use next/og or install @vercel/og?
Use next/og for modern Next.js App Router projects because it integrates with metadata image routes. @vercel/og remains useful for other frameworks and some legacy Next.js implementations requiring a separate endpoint.
Why does LinkedIn or Facebook show an older image?
Social platforms cache page metadata and images independently from our application. Use their inspection tools to request another crawl, but remember that existing posts may retain the preview captured when they were published.
Can we personalize an OG image for each signed-in user?
We generally should not. Social crawlers are unauthenticated, and their cached previews are public. Generate images from public page data, never from sessions, private profiles, account details, or permission-protected application state.
Conclusion
Dynamic Open Graph images are most valuable when they remove repetitive design work without making the sharing pipeline fragile. Next.js gives us a clean way to place that logic beside each route, pull from the same content source as the page, and generate a consistent image through ImageResponse.
The production details make the difference: absolute URLs, bounded content, supported CSS, small assets, predictable caching, public accessibility, and testing with real crawlers.
Once those foundations are in place, one well-designed template can improve the presentation of every article, product, job listing, case study, or public profile the site publishes.



