OG Pilot
Next.js OG image

Next.js OG image: metadata file vs ImageResponse vs hosted API

A Next.js OG image can be a file in the route, JSX from ImageResponse / @vercel/og, or a CDN URL from a hosted API. This page is the decision guide: when the metadata file convention is enough, when vercel/og is the right renderer, and when OG Pilot wins because the same card has to leave Next.js.

When to use each Next.js OG image path

Metadata file (opengraph-image)

  • One designed PNG or JPG covers the whole segment (about, pricing, a campaign).
  • You want Next.js to emit og:image, type, width, height, and alt without a metadata export.
  • The card changes with the route, and colocating opengraph-image.tsx next to the page is clearer than a shared API route.

ImageResponse / @vercel/og

  • The layout is JSX you want in the Next.js repo.
  • Many pages can share one Route Handler, with the title passed as a query param.
  • You accept the Satori CSS subset, the 500KB bundle cap, and Vercel function hosting.

Hosted OG image API

  • Rails, Astro, WordPress, or a non-Vercel host need the same templates.
  • Editors should pick a template, not maintain Edge CSS.
  • An agent should generate the card through MCP.

Feature comparison

Limits below are from the Next.js and Vercel docs linked in each section. OG Pilot cache behavior is from the API docs. Latency and public list prices stay marked until there is a source.

DimensionMetadata fileImageResponse / @vercel/ogHosted OG Pilot API
What you addopengraph-image or twitter-image in the route segmentJSX returned from ImageResponse (next/og or @vercel/og)A CDN URL from generateMetadata / openGraph.images
Who writes the meta tagsNext.js writes og:image, type, width, height, and altThe file convention writes them. A custom route does not.You set openGraph.images and twitter.images
Dynamic per URLA static image is one card per segment. A code file can read params.Yes, from params, fetch, or query stringYes. Pass template, title, description, and path per page.
Where it runsYour Next.js build or serverYour Next.js route, or @vercel/og on another framework on VercelOG Pilot CDN. The signing app can be anywhere.
How you design the cardA finished PNG/JPG, or JSX inside opengraph-image.tsxJSX plus a flexbox CSS subsetNamed templates such as page, blog_post, and product
CSS and size limitsStatic opengraph-image max 8MB. twitter-image max 5MB. Code files share the ImageResponse limits.No display:grid. Bundle max 500KB including JSX, CSS, fonts, and images.Template fields. You do not author per-request CSS.
FontsBaked into a static image, or ttf / otf / woff inside ImageResponsettf, otf, and woff. ttf or otf is preferred.Handled by the template
CachingGenerated images are statically optimized unless they use request-time APIs or uncached data.Production @vercel/og sets cache-control: public, immutable, no-transform, max-age=31536000.CDN URL. Optional iat refreshes the cache for 24 hours from that timestamp.
Other frameworks and agentsNext.js App Router onlyVercel documents @vercel/og for any framework deployed on Vercel. MCP is yours to build.JS, Ruby, Python, PHP, WordPress, and MCP at https://ogpilot.com/mcp
LatencyNEED VERIFYNEED VERIFYNEED VERIFY
CostNEED CITE from your hostNEED CITE from your function and bandwidth planNEED CITE. Current plans are in billing after signup.

Recommended OG size on the Vercel side is 1200×630 (Vercel OG Image Generation). ImageResponse defaults to that size too.

Metadata file: opengraph-image and twitter-image

Next.js App Router treats opengraph-image and twitter-image as file conventions. Drop a static .jpg, .jpeg, .png, or .gif in the segment and Next.js adds the meta tags. Put the alt text in opengraph-image.alt.txt or twitter-image.alt.txt. The build fails if a static opengraph-image is over 8MB or a static twitter-image is over 5MB.

A code file (.js, .ts, .tsx) default-exports a function that returns a Response. ImageResponse satisfies that. Optional exports are alt, size, and contentType. Generated images are statically optimized unless they touch request-time APIs or uncached data. generateImageMetadata returns more than one image from the same file. Since Next.js 16, params is a Promise. The convention shipped in v13.3.0.

Source: Next.js opengraph-image.

Static file convention

app/blog/[slug]/opengraph-image.png
app/blog/[slug]/opengraph-image.alt.txt   ← plain text alt

Next.js adds, for that segment:
<meta property="og:image" content="<generated>" />
<meta property="og:image:type" content="<generated>" />
<meta property="og:image:width" content="<generated>" />
<meta property="og:image:height" content="<generated>" />
<meta property="og:image:alt" content="text from the .alt.txt file" />

twitter-image.(jpg|jpeg|png|gif) does the same for twitter:image.
Supported static types: .jpg, .jpeg, .png, .gif.

Dynamic opengraph-image.tsx (ImageResponse)

import { ImageResponse } from "next/og"

export const alt = "Blog post"
export const size = { width: 1200, height: 630 }
export const contentType = "image/png"

// Next.js 16: params is a Promise. On 15 and earlier it is a plain object.
export default async function Image({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 64,
          background: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          padding: 48,
        }}
      >
        {post.title}
      </div>
    ),
    { ...size },
  )
}

You do not also set openGraph.images for a file in the same segment. Next.js already emits the tags. Reach for generateMetadata when the image URL is external, including a hosted API.

ImageResponse and @vercel/og

ImageResponse from next/og uses @vercel/og, Satori, and Resvg to convert JSX into PNG. The default size is 1200×630. Only flexbox and a CSS subset work. display: grid does not. The bundle, including JSX, CSS, fonts, and images, must stay under 500KB. Fonts are ttf, otf, and woff, and ttf or otf parse faster. The supported CSS list lives in Satori.

App Router projects already include @vercel/og. Other frameworks install the package. Vercel documents it as compatible with any framework deployed on Vercel, and as supported on the Node.js runtime. Pages Router plus the Node.js runtime does not support return new Response(â€Ļ) with vercel/og. App Router with Node.js or Edge, and Pages Router with Edge, do. In production, @vercel/og sets cache-control: public, immutable, no-transform, max-age=31536000. Change the URL when the artwork must change. If crawlers fetch /api/og, allow that path in robots.txt.

Sources: Next.js ImageResponse, Vercel OG Image Generation, @vercel/og API.

Shared Route Handler plus generateMetadata

// app/api/og/route.tsx
import { ImageResponse } from "next/og"
// App Router includes @vercel/og. No separate install.

export function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const title = searchParams.get("title") ?? "Hello"

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 56,
          background: "#fff7ed",
          width: "100%",
          height: "100%",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        {title}
      </div>
    ),
    { width: 1200, height: 630 },
  )
}

// app/blog/[slug]/page.tsx — point metadata at the absolute URL
export async function generateMetadata() {
  const ogImage = "https://example.com/api/og?title=Hello"
  return {
    openGraph: { images: [ogImage] },
    twitter: { card: "summary_large_image", images: [ogImage] },
  }
}

When a hosted OG image API wins

OG Pilot returns a CDN URL you place in og:image. Signing stays on your server: the JavaScript SDK reads OG_PILOT_API_KEY and OG_PILOT_DOMAIN, signs a JWT, and createImage returns the URL. buildPathFromNextProps fills the path claim from App Router props. Pass iat when you want the documented 24-hour cache window. Omit it and the cache does not expire on its own. Social crawlers then fetch the image from the CDN, which is the caching model for multi-framework sites that are not on Vercel.

  • Same templates on Next.js, Rails, Astro, Python, PHP, and WordPress.
  • Design stays in templates (blog_post, product, page, and others) instead of a 500KB Satori bundle.
  • Agents call the remote MCP endpoint. Install steps: /docs/mcp.

Source: https://ogpilot.com/docs. The same API is what the Rails and Astro guides use. WordPress sites can use the plugin instead of generateMetadata.

Next.js + og-pilot-js (server only)

import { buildPathFromNextProps, configure, createImage } from "og-pilot-js"

configure((config) => {
  config.apiKey = process.env.OG_PILOT_API_KEY
  config.domain = process.env.OG_PILOT_DOMAIN
})

export async function generateMetadata(props) {
  const path = await buildPathFromNextProps("/blog/[slug]", props)
  const ogImage = await createImage(
    {
      template: "blog_post",
      title: "Post title",
      description: "Post excerpt",
    },
    { path, iat: Date.now() },
  )

  return {
    openGraph: { images: [ogImage] },
    twitter: { card: "summary_large_image", images: [ogImage] },
  }
}

Raw HTTP shape

POST https://ogpilot.com/api/v1/images
Content-Type: application/json

{"token":"YOUR_JWT_TOKEN"}

Keep the API key out of client components. Prefer the SDK over hand-rolled JWT signing.

MCP install pointer

The remote endpoint is https://ogpilot.com/mcp. Documented tools include generate_og_image, debug_open_graph, compress_image, and extract_color_palette. Connector JSON differs by client, so follow /docs/mcp instead of a guessed config. After you publish a card, check it in the Open Graph debugger.

MCP endpoint pointer

Remote MCP URL: https://ogpilot.com/mcp
Docs: https://ogpilot.com/docs/mcp
OAuth: follow the custom connector guide on /docs/mcp (OAuth 2.1 + PKCE)

Latency and cost — labeled, not invented

  • Latency: NEED VERIFY. Time a metadata-file hit, a Vercel function region, and an OG Pilot CDN URL on your own pages. A warm social cache is a different number from the first fetch.
  • Cost: NEED CITE. Function and bandwidth prices belong to your host's pricing page. OG Pilot plans are shown in billing after signup. This page does not publish a cost per 1,000 images.
  • Engineering cost is the part the matrix cannot price: Satori CSS, font subsetting, and the 500KB cap versus a template you do not host.

Decision checklist

  1. One unchanging card for the segment? Add a static opengraph-image file and an alt text file.
  2. Card follows the route, and Next.js should write the tags? Use opengraph-image.tsx.
  3. Many routes, one JSX layout, still on Next.js? Use a Route Handler and generateMetadata.
  4. More than one framework, a non-Vercel host, or an agent? Use the hosted API and MCP.
  5. Put an absolute og:image URL in the HTML, then re-scrape after the artwork changes.

Ship the Next.js OG image path you actually need

Pick the metadata file or @vercel/og when the card lives entirely in one Next.js app. Get an API key when the preview also has to exist for Rails, Astro, WordPress, or an MCP client. Astro uses that same CDN URL. Generate a sample on /new if you want to see the template before you install the SDK.

  • Get an API key

    Create an account, verify a domain, and issue a signing key.

  • Use MCP

    Install the remote connector so an agent can generate the card.

  • Generate one image

    Preview a branded card before you wire production auth.

  • API docs

    JWT claims, templates, iat cache rules, and the JavaScript SDK.

  • Open Graph debugger

    Confirm the card before Facebook, X, LinkedIn, or Slack cache it.

  • Rails guide

    The same hosted API from a Rails layout, outside ImageResponse.

Next.js OG image questions

What is a Next.js opengraph-image file?

A special file in an App Router segment. A static opengraph-image.jpg, .jpeg, .png, or .gif, or a code file opengraph-image.js, .ts, or .tsx, makes Next.js add og:image tags for that segment. twitter-image does the same for Twitter cards. Static files must stay within the documented limits: 8MB for opengraph-image and 5MB for twitter-image.

Is ImageResponse the same as @vercel/og?

ImageResponse is the constructor. Next.js documents that it uses @vercel/og, Satori, and Resvg to turn JSX into PNG. In the App Router, import it from next/og. The @vercel/og package is already included there. Outside the App Router, Vercel documents installing @vercel/og yourself.

When should I use a metadata file instead of a Route Handler?

Use opengraph-image or twitter-image when the card belongs to that route segment and you want Next.js to write the meta tags. Use a Route Handler such as app/api/og/route.tsx when many pages share one ImageResponse endpoint, then point generateMetadata at that absolute URL.

When does a hosted OG image API beat @vercel/og?

When the same branded cards must ship from Rails, Astro, WordPress, or a non-Vercel host, when people should pick templates instead of Satori CSS, or when an agent should call MCP at https://ogpilot.com/mcp. @vercel/og remains a strong fit for one Next.js app that owns its JSX layouts.

Can generateMetadata point at an OG Pilot URL?

Yes. Sign the request on the server with og-pilot-js and return the CDN URL from openGraph.images and twitter.images. Keep the API key on the server. Setup is documented at /docs, and MCP install steps are at /docs/mcp.

Will this page publish latency or price benchmarks?

No. Figures that are not from official docs or a measured run are labeled NEED VERIFY or NEED CITE. Check in-app billing after /sign_up for current OG Pilot plans.

Get an API key or connect MCP

Keep opengraph-image or ImageResponse for a single Next.js app. Use OG Pilot when the card has to travel with an API key or an MCP install.