Open Graph в Next.js 15: превью лендинга для Telegram и соцсетей
Вы запустили лендинг с зелёными Core Web Vitals, JSON-LD и Partial Prerendering — а ссылка в Telegram показывает серый прямоугольник без картинки и обрезанный заголовок. Пользователь не кликает, менеджер по продажам пересылает ссылку в чат — и теряется половина смысла оффера. Для коммерческого лендинга Open Graph — не «косметика для Facebook», а часть конверсионной воронки: превью карточки влияет на CTR из мессенджеров, рассылок и органики в соцсетях.
В Next.js 15 App Router метаданные задаются через generateMetadata и файловые конвенции (opengraph-image.tsx). Это удобнее, чем ручные <meta> в _document, но легко получить дубли title/description, устаревший кеш OG-картинки или конфликт с canonical. В статье — production-подход для SEO-лендинга: статические и динамические OG-теги, генерация изображений 1200×630, особенности Telegram и проверка перед релизом.
Почему OG важнее, чем кажется SEO-специалисту
Google не использует og:title для ранжирования напрямую, но поведенческие сигналы от трафика из мессенджеров реальны. Telegram, WhatsApp, VK, Slack и Notion при расшаривании ссылки читают Open Graph (и иногда Twitter Cards как fallback). Если теги отсутствуют, парсер берёт первый <h1> и случайную картинку со страницы — часто логотип 64×64, растянутый в некрасивный thumbnail.
| Элемент превью | Рекомендуемый размер | Что ломается без настройки |
|---|---|---|
og:image | 1200×630 px (1.91:1) | Мелкий favicon или hero без crop |
og:title | 40–60 символов | Обрезка на мобильном Telegram |
og:description | 120–155 символов | Первый абзац <p> с навигацией |
og:url | Canonical URL | Дубли с UTM без canonical |
Для лендинга с одним оффером («Разработка сайтов за 14 дней») OG-карточка — мини-рекламный баннер. Заголовок должен совпадать по смыслу с <title>, но не обязан дословно повторять его: в <title> — ключ для поиска, в og:title — hook для человека в чате.
Twitter Cards (twitter:card, twitter:image) по-прежнему нужны: X и часть агрегаторов читают их первыми. Next.js 15 маппит поля openGraph и twitter из одного объекта Metadata — дублировать вручную не требуется, если настроить оба блока.
generateMetadata: статика, динамика и типичные ошибки
В App Router метаданные страницы задаются экспортом metadata (статика) или generateMetadata (динамика по params/searchParams):
// app/(marketing)/page.tsx — Server Component
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Разработка сайтов под ключ — срок 14 дней',
description:
'Next.js 15, Core Web Vitals в зелёной зоне, SEO и аналитика. Фиксированная смета и деплой на Vercel.',
metadataBase: new URL('https://example.com'),
alternates: {
canonical: '/',
},
openGraph: {
title: 'Сайт за 14 дней — без просадки по скорости',
description:
'Лендинг с INP ≤ 200 ms, JSON-LD и headless CMS. Обсудим проект за 15 минут.',
url: '/',
siteName: 'Example Studio',
locale: 'ru_RU',
type: 'website',
images: [
{
url: '/og/landing-default.png',
width: 1200,
height: 630,
alt: 'Пример SEO-лендинга на Next.js 15',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Сайт за 14 дней — без просадки по скорости',
description:
'Лендинг с INP ≤ 200 ms, JSON-LD и headless CMS.',
images: ['/og/landing-default.png'],
},
};
Три ошибки, которые видим на аудитах:
- Нет
metadataBase— относительные пути вog:imageпревращаются вhttps://localhost:3000/og/...на staging. - Title template дублируется — в
layout.tsxзаданtitle.template: '%s | Studio', а вopenGraph.titleтот же суффикс добавлен вручную. Telegram показывает «Заголовок | Studio | Studio». generateMetadataбезcache— при интеграции с headless CMS каждый запрос бота Telegram бьёт в API. Используйтеfetchс{ next: { revalidate: 3600 } }или статическую генерацию.
Для десятков landing pages по шаблону — динамический экспорт:
// app/(marketing)/services/[slug]/page.tsx
import type { Metadata } from 'next';
import { getServiceBySlug } from '@/lib/cms';
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const service = await getServiceBySlug(slug);
if (!service) return { title: 'Услуга не найдена' };
return {
title: service.seoTitle,
description: service.seoDescription,
alternates: { canonical: `/services/${slug}` },
openGraph: {
title: service.ogTitle ?? service.seoTitle,
description: service.ogDescription ?? service.seoDescription,
url: `/services/${slug}`,
images: [{ url: `/og/services/${slug}`, width: 1200, height: 630 }],
},
};
}
Важно: робот Telegram не выполняет JavaScript. OG-теги должны быть в исходном HTML ответа сервера. Client Components и 'use client' на page.tsx не генерируют meta — только Server Components и generateMetadata.
Если нужна подобная разработка — напишите в Telegram.
OG-изображения: opengraph-image.tsx и @vercel/og
Статический PNG в /public/og/ — быстрый старт, но при 50+ услугах не масштабируется. Next.js 15 поддерживает file-based metadata:
app/
├── (marketing)/
│ ├── opengraph-image.tsx # дефолт для секции
│ ├── page.tsx
│ └── services/
│ └── [slug]/
│ ├── opengraph-image.tsx
│ └── page.tsx
Пример генерации через ImageResponse (Satori + @vercel/og):
// app/(marketing)/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const alt = 'Example Studio — разработка сайтов';
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 0%, #1e293b 100%)',
color: '#f8fafc',
fontFamily: 'system-ui, sans-serif',
}}
>
<div style={{ fontSize: 56, fontWeight: 700, lineHeight: 1.15 }}>
Сайт за 14 дней
</div>
<div style={{ fontSize: 28, marginTop: 24, opacity: 0.85 }}>
Next.js 15 · Core Web Vitals · SEO
</div>
</div>
),
{ ...size }
);
}
Edge runtime здесь уместен: генерация лёгкая, cold start короткий, картинка кешируется CDN Vercel после первого запроса. Не смешивайте тяжёлые шрифты (multi-weight WOFF) — embed один weight через fetch локального TTF.
Для [slug]/opengraph-image.tsx принимайте params и подставляйте название услуги из CMS. Route handler app/og/[slug]/route.tsx — альтернатива, если нужны query-параметры (?title=), но file convention проще для SEO: URL предсказуемый, Next автоматически проставляет og:image в metadata.
Правила дизайна OG под Telegram:
- Safe zone — текст и логотип в центральных 80%, края могут обрезаться в круглых preview.
- Конtrast — мелкий серый текст на градиенте не читается в thumbnail 200 px шириной.
- Без текста мелче 24 px в макете 1200×630 — после даунскейла станет нечитаемым.
Telegram, VK и отладка кеша превью
Telegram кеширует превью агрессивно. После деплоя новых OG-тегов ссылка в чате может показывать старую картинку сутками. Обходные пути:
- Добавить версионный query только для теста:
?v=2— бот запросит свежий HTML. В production canonical иog:urlоставляйте без query, иначе дубли URL. - Использовать Telegram Bot API
sendMessageсlink_preview_options— для ботов, не для пользовательских шарингов. - Проверять через opengraph.xyz, metatags.io или self-hosted
curl -A "TelegramBot"— user-agent иногда влияет на отдачу.
VK и Facebook Meta Sharing Debugger сбрасывают кеш по кнопке «Scrape Again». Для VK отдельно проверьте og:image с HTTPS и размером ≥ 537×240.
Чеклист перед релизом лендинга:
# Исходный HTML содержит og:image с абсолютным URL
curl -sL https://example.com | grep -E 'og:(title|description|image|url)'
# Размер и тип картинки
curl -sI https://example.com/opengraph-image | grep -i content-type
В Search Console OG не отображается, но проверяйте URL Inspection → View crawled page — Google видит те же meta. Расхождение между title и og:title допустимо; расхождение между og:url и canonical — сигнал дубля.
Для мультиязычного лендинга добавьте openGraph.locale и alternates.languages — Telegram locale не переключает превью автоматически, но Facebook и LinkedIn используют hreflang-подсказки при выборе версии.
Связка с JSON-LD, PPR и headless CMS
OG и JSON-LD решают разные задачи: первый — превью в соцсетях, второй — rich snippets в SERP. Дублируйте смысл оффера, но не копируйте byte-в-byte: JSON-LD WebPage.description может быть длиннее, чем og:description.
При Partial Prerendering статическая оболочка включает <head> с metadata — бот Telegram получает OG мгновенно, без ожидания dynamic shell. Dynamic части страницы (личный кабинет, A/B-блок) не должны влиять на generateMetadata — выносите их в nested layouts без переопределения root metadata.
С Sanity, Contentful или Strapi храните ogTitle, ogDescription, ogImage отдельными полями в CMS. Редактор маркетинга меняет hook для Telegram, не трогая SEO title для Google. Revalidation (revalidateTag('service-${slug}')) после publish обновляет и HTML, и opengraph-image route.
shadcn/ui и framer-motion на OG не влияют — они client-side. Не тратьте время на отключение анимаций ради OG; тратьте на server-rendered head.
Нужна помощь? Telegram → или vic.kell@ya.ru
FAQ
Нужны ли отдельные og:title и document title?
Рекомендуется. <title> оптимизируется под ключевые слова и бренд (Разработка сайтов Москва | Studio). og:title — короткий hook для ленты (Сайт за 14 дней — смета в день обращения). Полное совпадение не ошибка, но упущенная возможность повысить CTR в мессенджерах.
Почему Telegram не видит og:image с Vercel?
Частые причины: относительный URL без metadataBase, редирект 307 на auth middleware, robots.txt блокирует /opengraph-image, image > 8 MB, или MIME-type не image/png / image/jpeg. Проверьте curl -sI URL с production-домена, не localhost.
Можно ли использовать hero WebP из next/image как og:image?
Не напрямую. next/image отдаёт оптимизированный URL с query-параметрами; не все боты его понимают. Для OG — статический PNG/JPEG или opengraph-image.tsx. Hero и OG могут визуально совпадать, но URL разные.
Как часто обновлять OG при контенте из CMS?
Синхронизируйте с ISR: revalidate: 3600 или on-demand revalidation при publish. Telegram-кеш живёт отдельно — после смены картинки предупредите команду, что preview в старых сообщениях не обновится.
Нужен ли og:video для лендинга?
Только если hero — полноценное промо-видео и вы целитесь в Facebook/LinkedIn video cards. Для типового B2B-лендинга summary_large_image достаточно; video утяжеляет парсинг и часто не поддерживается в Telegram.
Конфликтует ли opengraph-image.tsx с metadata.openGraph.images?
Next.js мерджит file-based и exported metadata. Если заданы оба, приоритет у file convention в той же папке. Держите один источник правды: либо PNG в metadata, либо opengraph-image.tsx, не оба сразу.
Заключение
Open Graph — часть упаковки лендинга наравне с LCP и JSON-LD. В Next.js 15 App Router достаточно metadataBase, продуманного generateMetadata, file-based opengraph-image.tsx на edge и проверки исходного HTML через curl. Telegram и VK не простят отсутствующую картинку — пользователь не кликнет, даже если сайт летает.
Настройте OG до запуска рекламы и рассылок: один раз сгенерируйте карточку 1200×630, проверьте absolute URL, сбросьте кеш в отладчиках — и каждая пересланная ссылка работает как мини-баннер вашего оффера.