DEV Community

Cover image for OG Images Done Right — How I Made Every Shared Link Work Harder
as1as
as1as

Posted on

OG Images Done Right — How I Made Every Shared Link Work Harder

One thing I learned building TalkWith.chat:

No matter how good your product is, if sharing a link on KakaoTalk or Slack shows no image — nobody clicks.

A single OG image determines your click-through rate. So I decided to do it properly.


Everyone Knows What OG Images Are

The image specified in <meta property="og:image"> that appears as a preview when sharing links. Works on Twitter, Slack, KakaoTalk, Discord — everywhere.

The problem with static images is that every page shows the same thumbnail. On a debate platform where a new topic drops every day, having every topic page share the same image is pointless.

"What if the topic title was baked into the OG image?" That was the starting point.


Dynamic OG Images in Next.js: opengraph-image.tsx

Since Next.js 13 App Router, placing an opengraph-image.tsx file in a folder automatically registers it as that page's OG image. Inside that file, you use ImageResponse to generate an image from JSX.

// app/[locale]/topic/[date]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

export const runtime = 'edge'
export const size = { width: 1200, height: 630 }

export default async function OGImage({ params }) {
  const topic = await getTopic(params.date)

  return new ImageResponse(
    (
      <div style={{
        background: '#050508',
        width: '100%',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        padding: '60px',
      }}>
        <p style={{ color: '#00f0ff', fontSize: 24 }}>TODAY'S BATTLE</p>
        <h1 style={{ color: 'white', fontSize: 48 }}>
          {topic.title}
        </h1>
        <div style={{ color: '#00f0ff', fontSize: 28, marginTop: 'auto' }}>
          ▶ JOIN THE DEBATE
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  )
}
Enter fullscreen mode Exit fullscreen mode

It runs on Edge Runtime so it's fast, and writing plain JSX makes designing straightforward.


What I Learned Building This

1. SEO title and OG title should be different

At first I used the SEO title directly as the OG title. But a title like "Should the U.S. government prioritize national security alerts over diplomatic engagement with Mexico? | TalkWith.chat" gets brutally truncated in a KakaoTalk share preview.

SEO title is for search results. OG title is for share previews. Manage them separately.

export async function generateMetadata({ params }) {
  const topic = await getTopic(params.date)

  return {
    title: `${topic.title} | TalkWith.chat`,  // SEO
    openGraph: {
      title: 'My AI debates for me.',  // Share preview — short and punchy
      description: topic.description.slice(0, 160),
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Custom fonts need to be loaded manually

ImageResponse only has access to system fonts by default. For custom fonts, you need to fetch the font file and pass it in explicitly.

const font = await fetch(
  new URL('/fonts/Orbitron-Bold.ttf', process.env.NEXT_PUBLIC_URL)
).then(res => res.arrayBuffer())

return new ImageResponse(jsx, {
  width: 1200,
  height: 630,
  fonts: [{ name: 'Orbitron', data: font, weight: 700 }]
})
Enter fullscreen mode Exit fullscreen mode

I spent a while confused about why my font wasn't rendering before I figured this out.

3. A CTA button changes everything

My first OG image just showed the topic title. Feedback came back: "I see the link preview but I don't know what I'm supposed to do."

Adding ▶ JOIN FREE NOW at the bottom made a real difference. Think of OG images as mini ad banners. A title-only image and a title-plus-CTA image perform differently.


OG Image Strategy by Page Type

I took a different approach for each page type on TalkWith.chat:

Page OG Image Strategy
Home (/) Today's topic title + CTA
Topic detail (/topic/[date]) Topic title + PRO/CON stance + CTA
User profile (/user/[username]) Username + level + stats
Static pages (about, privacy) Fixed brand image

The more dynamic the page, the more value there is in putting that page's core content into the OG image.


OG Image Checklist

Things to verify once you've built it:

Testing tools:

Common mistakes:

  • Image dimensions: 1200×630px is the standard. Smaller images render blurry
  • Cache issues: updated your OG image but the old one keeps showing? Each platform caches aggressively — you need to manually clear it per platform
  • Make sure og:image uses an absolute URL. Relative paths silently fail on some platforms

Final Thoughts

OG images feel like a set-it-and-forget-it thing once they're in place. But in practice, they determine the first impression every single time your link gets shared.

Don't use one static image for everything. Putting the right dynamic content in each page's OG image is what actually drives clicks.

Next.js opengraph-image.tsx is easier to implement than it looks. If you haven't done it yet, now's a good time.

Top comments (0)