Open Graph in Next.js 15: Landing Page Previews That Actually Get Clicks
You shipped a fast landing page — green Core Web Vitals, structured data, maybe Partial Prerendering — and then someone drops the link in Slack. Gray box. Wrong title. No hook. The offer disappears.
For commercial landings, Open Graph is not a Facebook detail. Messengers, email tools, Notion, and LinkedIn all build link cards from OG tags. A weak preview kills CTR long before Google ranking enters the conversation.
Next.js 15 App Router centralizes this in generateMetadata and file conventions like opengraph-image.tsx. Used well, you get consistent cards across channels. Used poorly, you get duplicate titles, staging URLs in production meta, and cached thumbnails that never update.
This guide covers a production setup: static and dynamic metadata, 1200×630 image generation on the edge, messenger-specific quirks, and a pre-launch checklist.
Why OG Tags Matter for Landing Pages
Search engines do not rank on og:title, but traffic quality feeds back into the business metrics you care about. Telegram, WhatsApp, Slack, Discord, and VK read Open Graph when unfurling links. Skip OG and parsers grab whatever is handy — often a tiny favicon blown up or the first paragraph of body copy mixed with nav text.
| Preview element | Target | Without config |
|---|---|---|
og:image | 1200×630 (1.91:1) | Random on-page asset |
og:title | 40–60 chars | Truncated <h1> |
og:description | ~120–155 chars | Boilerplate footer text |
og:url | Canonical URL | UTM variants as duplicates |
Treat the card as a mini ad. Your <title> can target keywords; og:title should sell the click in a chat thread. Keep twitter:card set to summary_large_image — X and several aggregators prefer Twitter meta even when OG is present.
generateMetadata: Patterns and Pitfalls
Export metadata for fixed pages or generateMetadata when content comes from params or a CMS:
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Custom web development — 14-day delivery',
description:
'Next.js 15 landings with green Core Web Vitals, SEO, and analytics. Fixed scope, Vercel deploy.',
metadataBase: new URL('https://example.com'),
alternates: { canonical: '/' },
openGraph: {
title: 'Ship your landing in 14 days — no speed trade-offs',
description:
'INP under 200ms, JSON-LD, headless CMS. Book a 15-minute scoping call.',
url: '/',
siteName: 'Example Studio',
locale: 'en_US',
type: 'website',
images: [
{
url: '/og/landing-default.png',
width: 1200,
height: 630,
alt: 'Next.js 15 SEO landing example',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Ship your landing in 14 days',
description: 'INP under 200ms, JSON-LD, headless CMS.',
images: ['/og/landing-default.png'],
},
};
Three audit favorites:
- Missing
metadataBase— relativeog:imagepaths resolve to localhost on preview deploys. - Double brand suffix —
title.templatein root layout plus manual| StudioinopenGraph.title. - Uncached CMS fetches — Telegram’s crawler hammers your API on every unfurl. Cache with
revalidateor static generation.
For programmatic landings:
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const page = await getPageBySlug(slug);
if (!page) return { title: 'Not found' };
return {
title: page.seoTitle,
description: page.seoDescription,
alternates: { canonical: `/services/${slug}` },
openGraph: {
title: page.ogTitle ?? page.seoTitle,
description: page.ogDescription ?? page.seoDescription,
url: `/services/${slug}`,
images: [{ url: `/og/services/${slug}`, width: 1200, height: 630 }],
},
};
}
Critical: Telegram does not run JavaScript. Meta tags must appear in server HTML. Client-only pages never fix OG with useEffect.
If you need this implemented on a project — message on Telegram.
OG Images with opengraph-image.tsx
A PNG in /public works for one URL. It does not scale to dozens of service pages. Next.js file-based metadata generates images per route:
app/(marketing)/opengraph-image.tsx
app/(marketing)/services/[slug]/opengraph-image.tsx
Edge-generated example with ImageResponse:
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default function OgImage() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 64,
background: 'linear-gradient(135deg, #0f172a, #1e293b)',
color: '#f8fafc',
fontFamily: 'system-ui',
}}
>
<div style={{ fontSize: 56, fontWeight: 700 }}>14-day landing delivery</div>
<div style={{ fontSize: 28, marginTop: 24, opacity: 0.85 }}>
Next.js 15 · Core Web Vitals · SEO
</div>
</div>
),
{ ...size }
);
}
Edge runtime fits: short cold starts, CDN caching after first request. Embed one font weight — heavy WOFF bundles slow generation.
Design for small thumbnails: keep copy in the central 80%, use high contrast, avoid type smaller than 24px at 1200×630 width.
Debugging Messenger Cache
Telegram caches previews aggressively. New og:image files may not show in old threads for hours or days. For QA, append ?v=2 to the URL — not for production canonicals. Use Meta Sharing Debugger or opengraph.xyz to validate tags. Confirm absolute HTTPS image URLs with:
curl -sL https://example.com | grep -E 'og:(title|description|image|url)'
OG complements JSON-LD, not replaces it. JSON-LD feeds SERP rich results; OG feeds social cards. Align messaging, allow different lengths.
With Partial Prerendering, static <head> metadata ships immediately — crawlers see OG without waiting for dynamic shells. Store ogTitle, ogDescription, and art direction fields separately in your headless CMS so marketing can tune shares without rewriting SEO titles.
Need help? Telegram → or vic.kell@ya.ru
FAQ
Should og:title match the document title?
Not necessarily. Optimize <title> for search intent; write og:title for humans scrolling a feed. Identical strings work but waste an CTR optimization.
Why does Telegram miss my Vercel OG image?
Check: missing metadataBase, auth middleware redirecting bots, wrong MIME type, oversized file, or blocked path in robots.txt. Test response headers from the production domain.
Can I reuse next/image hero URLs for og:image?
Avoid it. Optimized image URLs with query params confuse some parsers. Generate dedicated OG assets via opengraph-image.tsx or static PNG/JPEG.
opengraph-image.tsx vs metadata.openGraph.images?
Pick one source of truth per route. Next merges file conventions with exported metadata; duplicates create confusion about which image is canonical.
Wrapping Up
Open Graph is packaging. A fast landing with bad previews still loses clicks in the channels where B2B deals often start — DMs, Slack, email forwards.
In Next.js 15: set metadataBase, implement generateMetadata, generate 1200×630 art on the edge, validate server HTML with curl, and test unfurls before ads go live. Every shared link should work like a banner for your offer.