How to Generate Open Graph Images for Every Page
Give every page a relevant social preview with static images or a reusable, data-driven image route. This guide shows the Next.js setup, metadata, caching, and verification steps.
To generate an Open Graph image for every page, give each page a page-specific og:image URL. For a few stable pages, add a static image in each route segment. For data-driven pages such as blog posts, generate the image from the route’s content with a reusable template. In Next.js App Router, the file convention is opengraph-image.tsx; it can read a route parameter and return an ImageResponse.
This guide implements that pattern for app/blog/[slug], explains static and externally generated alternatives, and shows how to check the deployed metadata and image. Open Graph’s protocol defines og:image and its related image properties; it recommends providing image alt text as well. See the Open Graph protocol reference.
Choose how each page gets an image
| Approach | Use it when | Tradeoff |
|---|---|---|
| Static file per route | The artwork is stable and can be prepared ahead of time. | Each variation is a separate asset to create and maintain. |
| Generated route image | The image should reflect a slug, post title, product, or other page data. | The route must handle missing data, layout constraints, and cache behavior. |
| Image URL from another service | Your site already produces images elsewhere, or you use a non-Next.js image service. | The page metadata must point to a public image URL that preview fetchers can reach. |
Choose based on the number of unique pages, whether images depend on page data, where fonts and data are available, and how image changes should invalidate cached output. A static file is usually the simplest choice when a section shares one design. A generated route is useful when each record needs different text or artwork.
Generate a unique image in Next.js App Router
The example uses a small in-memory post list so the route can run without a database. Replace getPost with the same data source your blog uses. This file belongs at app/blog/[slug]/opengraph-image.tsx.
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const alt = 'A preview image for this blog post'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
const posts: Record<string, { title: string; category: string }> = {
'generate-og-images': {
title: 'How to Generate Open Graph Images for Every Page',
category: 'Development guide',
},
'metadata-basics': {
title: 'Metadata Basics for the App Router',
category: 'Next.js',
},
}
async function getPost(slug: string) {
return posts[slug] ?? null
}
export default async function OpenGraphImage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
const title = post?.title ?? 'Blog post'
const category = post?.category ?? 'Development guide'
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '72px',
background: '#101827',
color: '#ffffff',
fontFamily: 'sans-serif',
}}
>
<div style={{ display: 'flex', fontSize: 28, color: '#a7c7ff' }}>
{category}
</div>
<div
style={{
display: 'flex',
fontSize: 64,
lineHeight: 1.12,
fontWeight: 700,
overflow: 'hidden',
}}
>
{title}
</div>
<div style={{ display: 'flex', fontSize: 24, color: '#cbd5e1' }}>
Example Blog
</div>
</div>
),
{ ...size },
)
}
Next.js documents ImageResponse for rendering JSX and CSS into an image. Its documented example uses 1200 × 630 pixels; treat that as the framework example size, not a guarantee for every social platform. Its renderer supports a subset of CSS, including flexbox, so this example avoids CSS Grid and other advanced layout assumptions. Check the documentation for your installed Next.js version: the Next.js 15 reference also documents a 500 KB bundle limit, which is version-specific. See the metadata file convention and ImageResponse reference.
Use real route data
Replace the sample posts object with a lookup keyed by the slug. Return only the fields the template needs, and handle a missing record explicitly. The fallback above still produces a valid image; if unknown slugs should return an error instead, use the not-found handling appropriate to your route and verify that crawlers receive the intended result.
async function getPost(slug: string) {
const post = await db.post.findUnique({ where: { slug } })
if (!post) return null
return { title: post.title, category: post.category }
}
The database call is illustrative; adapt it to your project’s data layer. Keep the image lookup consistent with the page route so a renamed or deleted post does not leave a misleading preview. Avoid passing untrusted raw HTML into the rendering tree; render plain text fields.
Handle long titles and missing values
- Set a useful fallback title and category for missing optional fields.
- Keep title font size, line height, and available width controlled so long titles remain readable.
- Test short titles, long titles, punctuation, non-Latin characters if your audience uses them, and absent category or author data.
- Do not assume every generated image will fit a single line. Let the title wrap and leave enough space for the longest likely content.
- If a record does not exist, decide whether a generic image or a not-found response is correct for your site.
Set the page metadata
The file convention supplies Open Graph image metadata for the route. Set the page title and description from the same record using generateMetadata. Avoid defining competing image values in both the file convention and a metadata export: Next.js gives file-based metadata higher priority.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
async function getPost(slug: string) {
// Replace with the data lookup shared by your page and image route.
return {
title: slug === 'generate-og-images'
? 'How to Generate Open Graph Images for Every Page'
: 'Blog post',
description: 'A practical guide to page-specific Open Graph images.',
}
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.description,
}
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return <article><h1>{post.title}</h1><p>{post.description}</p></article>
}
In a real project, share the data helper rather than duplicating it. Next.js metadata is resolved on the server and can depend on route parameters and external data. See Next.js metadata and OG image documentation.
Use a static image for stable artwork
When a route’s image does not need to change with its content, add opengraph-image.jpg to that route segment. Next.js emits the corresponding image metadata. A more specific route image takes precedence over one higher in the route tree, which lets a section use a shared image while individual routes override it.
app/
opengraph-image.jpg # fallback for routes under app
blog/
opengraph-image.jpg # shared blog image
[slug]/
opengraph-image.jpg # optional per-route static image
Use this when artwork is manually designed or supplied by an editor. The documented convention has an 8 MB maximum image file size; Next.js says a larger file makes the build fail. Confirm current framework and platform requirements before choosing production asset limits.
Use an external image URL
If another service creates the image, set its absolute public URL in the page’s Open Graph metadata. The target should return the image directly to a remote fetcher without requiring a logged-in browser session.
import type { Metadata } from 'next'
export async function generateMetadata(): Promise<Metadata> {
const imageUrl = 'https://images.example.com/posts/example.png'
return {
openGraph: {
images: [{
url: imageUrl,
width: 1200,
height: 630,
alt: 'A visual preview of the example post',
}],
},
}
}
Replace the example host with your actual image service. Ensure the URL is stable, publicly fetchable, and updated when the underlying image changes. Open Graph defines structured properties for image type, dimensions, and alt text; supply accurate values and descriptive alt text where available.
Choose metadata and cache behavior
Generated images are optimized and cached by default in Next.js. Dynamic APIs, uncached data, or route configuration can change this behavior. Pick the behavior that matches how often the source content changes: a rarely edited blog post can use cached output, while frequently changing page data needs a deliberate invalidation or revalidation plan. The exact setup depends on your data source and Next.js version, so verify it against the framework documentation.
- Build or cached output: efficient for stable content, but remember to refresh the generated result when content changes.
- Request-time data: reflects current data more directly, but may add work to image requests and depend on the availability of that data source.
- External generated URL: separates image generation from page rendering, but adds a service and its own update and availability behavior.
Do not assume a content update has refreshed every social platform’s stored preview. This research establishes framework caching behavior, not platform-specific crawler refresh schedules. Check the deployed page and image first, then use the relevant platform’s preview tooling when diagnosing a stale share card.
Verify the deployed page and image
- Deploy the route and open the actual page URL.
- Inspect the rendered document head and confirm there is an
og:imagevalue for that page. - Check that the image URL is absolute or resolves correctly for external fetchers.
- Open the image URL directly. Confirm it returns an image rather than an error page or an authentication screen.
- Review the image visually for clipping, unreadable text, missing fonts, and unexpected fallback content.
- Compare two different slugs to confirm the generated image uses the corresponding page data.
- Use a social preview debugger or equivalent to inspect a real share preview; a successful direct request alone does not establish how every platform caches previews.
Use a browser capture to inspect the final page as a visitor sees it. For example, capture two deployed routes and check their metadata and rendered previews:
curl -L "https://example.com/blog/generate-og-images" -o page.html
rg -o '<meta[^>]+property="og:image"[^>]*>' page.html
Replace the domain and path with your deployed route. The command is a quick source check; server-rendered output and your framework’s metadata are what matter, so inspect the actual response rather than relying on client-side DOM state.
Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
No og:image in the page head |
The file convention is in the wrong route segment, or conflicting metadata is defined. | Check the route path and inspect resolved metadata. Remember file-based metadata has higher priority than metadata exports. |
| Every slug displays the same title | The image route is not reading the route parameter or its lookup always returns the same record. | Log or inspect the resolved slug and data lookup; test at least two known routes. |
| The image route fails for one post | The slug has no matching record or a required field is missing. | Return a deliberate fallback or not-found response, and make fields optional where appropriate. |
| Text is clipped or too small | The template assumes fixed title length or uses unsupported layout CSS. | Allow wrapping, constrain typography, test long content, and stick to supported CSS such as flexbox. |
| Generated image appears stale | Framework output or a preview platform may have cached an earlier result. | Review your Next.js data and route caching behavior, confirm the current image URL directly, and refresh the platform preview using its available tools. |
| Image URL works locally but not to a crawler | The URL may require authentication, use a private hostname, or return a redirect/error unsuitable to the fetcher. | Test the deployed URL from outside your authenticated browser and ensure it serves the image publicly. |
| Build fails on the static image | The asset may exceed the documented Next.js convention size limit or have an unsupported format. | Check the file size and format against the current framework documentation; the documented convention states an 8 MB maximum. |
| Image generation bundle is too large | Assets or dependencies may exceed the limit for the Next.js version in use. | Reduce bundled assets and consult the version-specific ImageResponse reference. The 500 KB figure in the Next.js 15 reference is not a universal current limit. |
Performance, reliability, and cost considerations
Static route images have the fewest runtime dependencies. Generated images centralize design and reduce the need to create each variation manually, while adding a data lookup and rendering path. External generation moves that work elsewhere but introduces a dependency on the image service and URL.
- Keep the image template small and avoid loading unnecessary assets into the renderer.
- Cache stable output, and decide how content edits cause image output to refresh.
- Handle missing data and renderer errors deliberately so one unusual record does not create a broken preview.
- Check actual output size, direct URL accessibility, and deployed behavior. Do not infer a click-through or SEO improvement from the presence of unique artwork; the reviewed sources establish metadata behavior, not measured performance outcomes.
- Account for the relevant framework version’s limits and the image service’s own pricing and limits if you use one. No third-party generator is required by the Next.js approach.
Or skip the browser setup
If you also need screenshots of the actual page, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF; see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/blog/generate-og-images -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/blog/generate-og-images"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/blog/generate-og-images',
});
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);
Cookie banners, newsletter popups, and chat widgets are removed before the shot, and each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does every page need a different image file?
No. A generated route can create different image output from each page’s data using one template. Static files are also valid when the artwork is stable.
Is 1200 × 630 required?
It is the size used in the Next.js documentation example, not a universal guarantee for all platforms. Choose dimensions based on the destinations you support and verify their current requirements.
Should the image alt text be the same as the post title?
Use a concise description of what the image contains. The Open Graph protocol describes alt text as an image description, not a caption.
Why does a share still show the previous image?
The image or metadata may be cached by the framework or by the preview platform. Confirm the deployed head and image first; platform-specific refresh timing is not established here.


