ScreenshotNeo

BlogHow-to

How to Create Custom Open Graph Images in Next.js

Create route-specific Open Graph images in Next.js with a static asset, metadata, or the App Router’s ImageResponse convention.

By the ScreenshotNeo team4 October 20269 min read

To create a custom Open Graph image in Next.js, choose the method that matches the image: put a fixed image in the relevant App Router segment, use opengraph-image.tsx and ImageResponse when the artwork needs route or content data, or set openGraph.images in metadata when an image already exists at a URL. Next.js adds the Open Graph metadata for file-convention images automatically.

This guide covers the App Router. The examples use the documented 1200 × 630 dimensions as a practical starting point, not a universal requirement for every social platform.

1. Choose the right implementation

Method Use it when Where it lives
Static image file The same designed image applies throughout a route segment. app/opengraph-image.jpg, .jpeg, .png, or .gif
Generated image The image should include a slug, title, author, or other route/content data. app/opengraph-image.tsx (or .js/.ts) returning ImageResponse
Metadata URL You already have an absolute image URL or are building all metadata together. metadata or generateMetadata in the route

More specific file-based metadata takes precedence over an image higher in the route tree. For example, a page under app/blog/[slug]/ can supply an image that overrides a broader app/opengraph-image.png. The file convention has been available since Next.js 13.3.0. See the Open Graph image file convention and the metadata and OG image guide.

2. Add a fixed Open Graph image

Design the image in an editor, export it as a supported file, and place it in the route segment it represents:

app/
  opengraph-image.png
  blog/
    opengraph-image.jpg
    [slug]/
      page.tsx

The root image applies broadly; the blog image is more specific for blog routes. Next.js derives the relevant Open Graph tags from the file. For a static image, add a sibling opengraph-image.alt.txt file if you want to provide descriptive alt text.

The file-convention reference documents an 8 MB maximum for a static opengraph-image; a file over the limit fails the build. Keep the image compressed and confirm it is in the intended route segment. Do not confuse this with the adjacent 5 MB limit documented for twitter-image.

3. Generate an image with route data

Use the ImageResponse API from next/og when each page needs a composed image. In Next.js 16, the route handler receives params as a promise. This example uses that version’s parameter shape and a local title map so it runs without a database or external service:

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

type Props = {
  params: Promise<{ slug: string }>
}

const posts: Record<string, { title: string; category: string }> = {
  'custom-open-graph-images-nextjs': {
    title: 'Custom Open Graph Images in Next.js',
    category: 'Next.js Guide',
  },
}

export const runtime = 'edge'
export const alt = 'Article title on a branded blue background'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function Image({ params }: Props) {
  const { slug } = await params
  const post = posts[slug]

  if (!post) {
    return new ImageResponse(
      <div style={{ display: 'flex', width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', background: '#101827', color: 'white', fontSize: 48 }}>
        Article not found
      </div>,
      size,
    )
  }

  return new ImageResponse(
    <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', width: '100%', height: '100%', padding: 72, background: '#101827', color: 'white' }}>
      <div style={{ display: 'flex', color: '#a5b4fc', fontSize: 28 }}>{post.category}</div>
      <div style={{ display: 'flex', maxWidth: 1000, fontSize: 68, fontWeight: 700, lineHeight: 1.1 }}>{post.title}</div>
      <div style={{ display: 'flex', color: '#cbd5e1', fontSize: 24 }}>example.com</div>
    </div>,
    { ...size },
  )
}

The example deliberately has a fallback for an unknown slug so a missing map entry does not cause an exception. In a real project, load the post from the data source that owns it and decide what should happen when no record exists: return a branded fallback, or handle the missing route according to your application’s routing behavior.

The official guide also demonstrates loading a local font with Node.js file APIs and passing it to ImageResponse. Use the font-loading approach appropriate to your chosen runtime and deployment. The image renderer supports a subset of CSS through Satori and resvg; it is not a full browser renderer. Flexbox and absolute positioning are among supported techniques, while CSS Grid is explicitly unsupported. Check the ImageResponse reference before relying on a styling feature.

4. Use external content safely

For dynamic posts, resolve the route parameter and fetch the record inside the image function. In Next.js 16 the parameter is asynchronous:

type Props = { params: Promise<{ slug: string }> }

export default async function Image({ params }: Props) {
  const { slug } = await params
  const post = await getPostBySlug(slug)

  if (!post) {
    // Return a fallback image or use the application's not-found strategy.
  }

  return new ImageResponse(/* JSX using post.title and post.author */, {
    width: 1200,
    height: 630,
  })
}

getPostBySlug is application-specific and is intentionally not presented as a built-in Next.js function. Handle fetch failures, missing fields, long titles, and unexpected characters. Clamp or wrap text within a known layout, and provide safe defaults for optional author or category values. If you copy code from an older Next.js example, check its parameter type against the installed version.

5. Set the image through metadata instead

If the image is already available at a stable absolute URL, set it in route metadata. This is useful when metadata fields are assembled together rather than derived from an opengraph-image file:

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

type Props = { params: Promise<{ slug: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const post = await getPostBySlug(slug)

  return {
    title: post.title,
    openGraph: {
      title: post.title,
      images: [
        {
          url: `https://example.com/og/${slug}.png`,
          width: 1200,
          height: 630,
          alt: `Open Graph image for ${post.title}`,
        },
      ],
    },
  }
}

Replace getPostBySlug and the image URL with your application’s real data and image route. The metadata reference documents image URL, dimensions, and alt text fields; file-based metadata may be more convenient when the image belongs to the route itself. See generateMetadata.

6. Add custom fonts and multiple image variants

For brand typography, load a local font and pass its bytes as a font entry in the ImageResponse options. The Next.js guide includes a Node.js file API example; verify runtime compatibility if you select Edge runtime. Keep font files available at build or execution time and avoid loading a large font when a smaller subset will render the required glyphs.

When a segment needs multiple image metadata entries, Next.js provides generateImageMetadata. It can return several entries for a segment; in version 16, its id and params values are promises. Follow the installed-version reference rather than assuming a synchronous signature. See generateImageMetadata.

7. Verify the generated metadata and image

  1. Start the app using its normal development or production command.
  2. Open the target page and inspect the rendered document head for an og:image URL and related metadata.
  3. Open the image URL directly. Confirm it returns an image and that the title, font, colors, and layout are correct.
  4. Check a nested route to confirm the intended segment’s image takes precedence.
  5. For generated output, exercise a valid slug, an unknown slug, and content with a long title.

Next.js supplies the appropriate tags for file-based metadata. This verification checks your app’s output; it does not establish how every social network fetches, caches, or displays a preview. Platform preview behavior can differ, so validate with the platform-specific preview tool when that behavior matters.

8. Caching, performance, reliability, and cost

Caching and freshness

Generated images are statically optimized and cached by default unless the route uses a Dynamic API or dynamic configuration. Uncached data can change that behavior. Review the route-segment and fetch caching configuration for your Next.js version and data source. If a post title changes, consider whether the image should update immediately or remain cached according to your chosen policy.

Generation cost

A static image avoids per-request composition work at runtime. A generated image adds data access and rendering to the request path when it is not served from cache. Keep data retrieval bounded, avoid unnecessary remote calls, and use the framework’s caching behavior deliberately. No performance benchmark is implied here; measure in your deployment if image response latency is important.

Reliability

Make the generator resilient to absent or malformed content. Use a fallback title, constrain text length, and avoid design dependencies that the renderer does not support. Keep required fonts and content data available in the runtime. A route that depends on an external API should account for that API’s errors and latency.

Image weight

For static files, stay under the documented 8 MB limit and prefer an efficient export. Generated responses in the documented example are PNG. Choose visual complexity and dimensions that suit the intended use; 1200 × 630 is the guide’s example, not a claim about every consumer’s requirements.

9. Troubleshooting

Symptom Likely cause Fix
Build fails on a static image The file exceeds the documented 8 MB OG image limit, has an unsupported extension, or is misplaced. Compress it, use a supported extension, and check the App Router segment path.
Image route throws while reading params Code assumes a plain object while using the Next.js 16 promise-based signature. Type params as a promise and await it; consult the reference for your installed version.
Layout is missing or distorted The design uses CSS outside the ImageResponse renderer’s supported subset, such as CSS Grid. Rebuild the layout with supported flexbox or absolute positioning styles.
Some titles overflow Content length or glyph width exceeds the designed text area. Wrap text, constrain its maximum length, adjust font sizing, or use a fallback layout.
Custom font does not appear The font was not loaded, is unavailable to the runtime, or is not passed correctly. Check the file path, runtime support, and font option shape in the ImageResponse reference.
Image shows stale content Static optimization or caching continues serving an earlier generated result. Review dynamic configuration and fetch/route caching for your version, then use a freshness policy that matches the content.
Nested page uses the wrong image The expected file is absent from the more-specific segment or metadata is set elsewhere. Inspect the route tree and rendered head; add the image at the intended segment or consolidate metadata ownership.
External data makes image generation fail The record is missing, the request failed, or required fields are empty. Handle non-success and missing-data cases explicitly and return a fallback or the app’s not-found result.

10. Or skip the browser setup

If you need a screenshot of a rendered page rather than a designed Open Graph card, ScreenshotNeo is a website screenshot API and MCP server. A GET request returns an image or PDF. It can also capture a URL after rendering; it does not replace the route-aware image composition shown above. 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}`);

Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

11. Frequently asked questions

Can I have a different Open Graph image for every blog post?

Yes. Put a generator in the dynamic route segment and use its slug to load the corresponding content, or set a per-page image URL in generateMetadata.

Does ImageResponse render a normal web page?

No. It creates an image using a supported subset of CSS; it is not a full browser screenshot engine.

Do I need to set Open Graph tags manually for a file-convention image?

Next.js generates the corresponding tags for the file convention. Inspect the rendered head to confirm the route resolves as intended.

Should I use a static file or generate the image?

Use a static file when the design is fixed. Generate an image when it needs route or content data, or use metadata when a suitable absolute image URL already exists.

Sources: Open Graph image file convention, Metadata and OG image guide, ImageResponse API, generateImageMetadata, and generateMetadata.