Blogs/Technology

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

Written byRitwik
Aug 17, 2026
12 Min Read
Boost Site Engagement with Dynamic Open Graph Images in Next.js Hero
Too Long? Read This First

- 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:title
  • og:type
  • og:image
  • og: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:

  • ImageResponse through next/og
  • File-based opengraph-image and twitter-image metadata routes
  • The metadata object and generateMetadata function
  • Automatic generation of the corresponding image metadata tags
  • generateImageMetadata when 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.tsx

When 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-images

The 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:

  1. Build the cache key from stable public data, such as the article slug and content version.
  2. Cache successful image responses at the CDN.
  3. Revalidate or change the version only when visible image content changes.
  4. 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 OK without authentication.
  • Check the final page source to confirm that the expected og:image and twitter:image tags are server-rendered.

Common Problems and Their Fixes

ProblemLikely causeFix
No image appears in the previewImage URL is relative, private, broken, or unavailable to the crawlerConfigure metadataBase, use HTTPS, and test the exact public image URL
The old image keeps appearingThe social platform cached an earlier crawlUse the platform's inspector or debugger and remember that existing posts may not update
Text is clippedTitle length and font size are fixedTruncate carefully, adjust type size by length, and keep a safe area
Build fails after adding fontsMetadata image bundle exceeds the asset limitSubset the font and include only required weights
CSS looks different from the browserImageResponse does not implement the complete browser CSS engineUse supported CSS, especially Flexbox, and simplify the layout
Image works locally but not in productionEnvironment-based origin is missing or incorrectSet and validate the production site URL during deployment
Every request regenerates the imageDynamic inputs or cache configuration prevent reuseUse stable identifiers and verify deployed cache headers
Scrapers trigger excessive workPublic query parameters create unlimited render combinationsValidate and bound inputs, or generate from server-side records by slug
No image appears in the preview
Likely cause
Image URL is relative, private, broken, or unavailable to the crawler
Fix
Configure metadataBase, use HTTPS, and test the exact public image URL
1 of 8

How to Test the Implementation

Do not stop after opening the image route in a browser. Test the complete sharing path.

  1. Open the deployed page and confirm that it returns 200 OK.
  2. View the rendered HTML source and find og:title, og:image, og:url, twitter:card, and twitter:image.
  3. Open the exact image URL in a private browser window.
  4. Check its content type, dimensions, and response time.
  5. Test a short title, long title, missing post, and non-Latin title if the site supports one.
  6. Run the URL through Meta's Sharing Debugger and LinkedIn's Post Inspector.
  7. Share it in a private Slack or Discord channel and on a test mobile chat.
  8. 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.

Author-Ritwik
Ritwik

Web Developer, cricket and finance enthusiast.

Share this article

Phone

Next for you

8 Best GraphQL Libraries for Node.js in 2025 Cover

Technology

Aug 4, 202613 min read

8 Best GraphQL Libraries for Node.js in 2025

8 Best GraphQL Libraries for Node.js in 2026 Too Long? Read This First - Choose Apollo Server when you need a mature ecosystem, GraphOS integration, plugins, or Apollo Federation. - Choose GraphQL Yoga for a modern, portable server with Fetch API compatibility and built-in support for subscriptions over Server-Sent Events. - Choose Mercurius when your application already uses Fastify and runtime efficiency is a major priority. - Use GraphQL.js when you need the official JavaScript implementati

9 React Native Animation Libraries and Tools Compared Cover

Technology

Aug 4, 202615 min read

9 React Native Animation Libraries and Tools Compared

Too Long? Read This First - Use React Native Reanimated for gesture-driven, interruptible, and performance-sensitive interface animations. - Use the built-in Animated API for simple fades, transforms, and timed sequences without another dependency. - Pair React Native Gesture Handler with Reanimated for swipes, dragging, pinching, rotation, and other touch-driven experiences. - Use Lottie React Native for non-interactive motion graphics supplied by designers. - Choose React Native Skia for cust

9 Critical Practices for Secure Web Application Development Cover

Technology

Aug 4, 202616 min read

9 Critical Practices for Secure Web Application Development

Too Long? Read This First - Define security requirements and model threats before implementation begins. - Treat authentication, account recovery, and MFA as one complete identity system. - Apply server-side authorization to every protected action and object. - Prevent injection with parameterized APIs, structured validation, safe output handling, and restricted outbound requests. - Protect sessions and tokens throughout their complete lifecycle. - Minimise sensitive data and manage encryption