JSON-LD в Next.js 15: rich snippets для SEO-лендинга
Лендинг может загружаться за 800 миллисекунд и получать 100 баллов в Lighthouse — но в выдаче Google он выглядит как обычная синяя ссылка. Конкурент с FAQ-аккордеоном прямо в SERP забирает клики, потому что у него настроена разметка FAQPage. Расширенные сниппеты (rich snippets) не гарантируют рост позиций, но повышают CTR на 15–30% в коммерческих нишах — особенно на мобильных экранах, где места под заголовок мало.
JSON-LD — рекомендуемый Google формат structured data. В Next.js 15 App Router его удобно встраивать через Server Components: разметка попадает в HTML при первом ответе, без ожидания JavaScript. Робот видит application/ld+json сразу, независимо от Partial Prerendering или чистого SSG.
В этой статье разберём production-подход: типы Schema.org для лендинга услуг, типизация на TypeScript, связка с Metadata API, валидация и типичные ошибки, которые ломают rich results.
Зачем JSON-LD на лендинге в 2026 году
Google Search поддерживает десятки типов Schema.org, но для типичного лендинга B2B-услуг достаточно трёх-пяти сущностей:
| Тип | Что даёт в выдаче | Когда нужен |
|---|---|---|
Organization / LocalBusiness | Панель знаний, логотип, контакты | Любой коммерческий сайт |
WebSite | Sitelinks search box | Сайты с поиском |
FAQPage | Раскрывающиеся вопросы в SERP | Лендинги с блоком FAQ |
Product / Service | Цена, рейтинг, availability | SaaS, услуги с тарифами |
BreadcrumbList | Хлебные крошки в сниппете | Многостраничные сайты |
С 2024 года Google сократил поддержку некоторых rich results (HowTo, Special Announcement), но FAQPage, Organization и Product по-прежнему работают. Алгоритм не «награждает» разметку напрямую — он использует её для понимания контента и форматирования сниппета. Дублирование текста FAQ на странице и в JSON-LD обязательно: разметка должна отражать видимый контент, иначе Manual Action за spam structured data.
Для Next.js 15 ключевое преимущество — JSON-LD рендерится на сервере вместе с HTML. Client-side injection через useEffect рискован: Googlebot иногда не дождётся скрипта, а Core Web Vitals страдают от лишнего JS.
Выбор схем: Organization, FAQPage, Service
Organization + WebSite
Базовая связка для любого лендинга. Organization описывает компанию, WebSite — сам сайт и потенциальный поиск:
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
'@id': 'https://example.com/#organization',
name: 'Kel IT',
url: 'https://example.com',
logo: 'https://example.com/logo.png',
sameAs: [
'https://t.me/dp_victor',
'https://github.com/kel-it',
],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'sales',
email: 'vic.kell@ya.ru',
availableLanguage: ['Russian', 'English'],
},
};
@id позволяет ссылаться на сущность из других блоков — Google рекомендует единый идентификатор для связанных типов.
FAQPage
Самый заметный rich result для лендинга. Каждый вопрос — Question с acceptedAnswer:
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqItems.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
};
Важно: вопросы в JSON-LD должны дословно совпадать с текстом на странице. Если FAQ рендерится из CMS — используйте один источник данных для UI и schema.
ProfessionalService
Для лендинга услуг (разработка сайтов, дизайн, консалтинг) подходит ProfessionalService — расширение LocalBusiness:
const serviceSchema = {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
name: 'Разработка сайтов на Next.js и Astro',
provider: { '@id': 'https://example.com/#organization' },
areaServed: { '@type': 'Country', name: 'Russia' },
priceRange: '$$',
description: 'SEO-лендинги с Core Web Vitals 90+',
};
Связь через @id на Organization избегает дублирования данных о компании.
Если нужна подобная разработка — напишите в Telegram.
Реализация в App Router: компонент и типизация
Создайте переиспользуемый Server Component для JSON-LD:
// components/json-ld.tsx
type JsonLdProps = {
data: Record<string, unknown> | Record<string, unknown>[];
};
export function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data),
}}
/>
);
}
dangerouslySetInnerHTML здесь безопасен: вы контролируете данные на сервере, пользовательский ввод не попадает в schema без санитизации.
Объединение нескольких схем
Google принимает массив объектов в одном script-теге или несколько отдельных тегов. Практичнее — @graph:
const graphSchema = {
'@context': 'https://schema.org',
'@graph': [
organizationSchema,
{
'@type': 'WebSite',
'@id': 'https://example.com/#website',
url: 'https://example.com',
name: 'Kel IT',
publisher: { '@id': 'https://example.com/#organization' },
},
faqSchema,
serviceSchema,
],
};
Типизация с schema-dts
Пакет schema-dts даёт TypeScript-типы для Schema.org:
npm install schema-dts
import type { WithContext, Organization, FAQPage } from 'schema-dts';
const org: WithContext<Organization> = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Kel IT',
url: 'https://example.com',
};
Типы не валидируют runtime-данные, но ловят опечатки в @type и обязательных полях на этапе компиляции.
Подключение на странице
// app/page.tsx
import { JsonLd } from '@/components/json-ld';
import { buildLandingSchema } from '@/lib/schema';
import { faqItems } from '@/content/faq';
export default function LandingPage() {
const schema = buildLandingSchema({ faqItems });
return (
<>
<JsonLd data={schema} />
<Hero />
<FAQ items={faqItems} />
{/* ... */}
</>
);
}
Размещайте <JsonLd /> в начале <body> или в layout — позиция не влияет на парсинг, но логичнее держать рядом с SEO-критичным контентом.
Динамические данные из CMS
Headless CMS (Sanity, Contentful, Strapi) — типичный источник FAQ и описаний услуг. Загружайте данные в Server Component:
// lib/schema.ts
export async function buildLandingSchema() {
const faq = await getFaqFromCms();
const settings = await getSiteSettings();
return {
'@context': 'https://schema.org',
'@graph': [
buildOrganization(settings),
buildFaqPage(faq),
],
};
}
При ISR (revalidate: 3600) schema обновляется вместе с HTML — не нужен отдельный pipeline.
Metadata API и согласованность с Open Graph
JSON-LD дополняет, но не заменяет Metadata API. Заголовок, description и canonical должны совпадать с данными в schema:
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Разработка сайтов — Next.js 15, Astro 5, SEO',
description: 'Лендинги с rich snippets и Core Web Vitals 90+. JSON-LD, edge deploy на Vercel.',
metadataBase: new URL('https://example.com'),
alternates: { canonical: '/' },
openGraph: {
title: 'Разработка сайтов — Kel IT',
description: 'SEO-лендинги на Next.js 15 и Astro 5',
url: 'https://example.com',
siteName: 'Kel IT',
locale: 'ru_RU',
type: 'website',
images: [{ url: '/og.png', width: 1200, height: 630 }],
},
};
Расхождение между metadata.description и Organization.description в JSON-LD сбивает с толку алгоритмы entity resolution. Держите единый источник:
// lib/seo.ts
export const siteConfig = {
name: 'Kel IT',
description: 'Лендинги с rich snippets и Core Web Vitals 90+',
url: 'https://example.com',
} as const;
// Используйте siteConfig и в metadata, и в buildOrganization()
hreflang для мультиязычных лендингов
Если есть EN/RU версии, добавьте alternates.languages в metadata и отдельные JSON-LD с inLanguage:
alternates: {
canonical: 'https://example.com',
languages: {
'ru-RU': 'https://example.com',
'en-US': 'https://example.com/en',
},
},
Валидация: Rich Results Test и Search Console
Перед деплоем проверяйте разметку в Google Rich Results Test. Инструмент показывает eligible rich result types и ошибки парсинга.
Чеклист после деплоя:
- View Source (не DevTools Elements) — убедитесь, что
<script type="application/ld+json">присутствует в исходном HTML - Rich Results Test по production URL — 0 errors, warnings разберите отдельно
- Search Console → Enhancements — мониторинг FAQ, Breadcrumbs, Organization
- URL Inspection → View crawled page — rendered HTML содержит schema
Автоматизируйте проверку в CI:
// __tests__/schema.test.ts
import { buildLandingSchema } from '@/lib/schema';
import { faqItems } from '@/content/faq';
describe('JSON-LD schema', () => {
it('FAQ questions match content source', () => {
const schema = buildLandingSchema({ faqItems });
const faq = schema['@graph'].find(
(item) => item['@type'] === 'FAQPage'
);
expect(faq.mainEntity).toHaveLength(faqItems.length);
faq.mainEntity.forEach((q, i) => {
expect(q.name).toBe(faqItems[i].question);
});
});
});
Тест не заменяет Rich Results Test, но ловит рассинхрон FAQ при рефакторинге.
Типичные ошибки и anti-patterns
Разметка невидимого контента. FAQ в JSON-LD, которых нет на странице — прямой путь к manual penalty. Google явно запрещает «markup that is misleading or not representative of the page content».
Client-side only injection. useEffect(() => { appendScript(schema) }) — schema может не попасть в crawled HTML. Только Server Components или SSG.
Дублирование противоречивых данных. Два Organization с разными name на одной странице — entity confusion. Используйте @graph с @id.
Product schema без реальных отзывов. aggregateRating с выдуманными 5.0/100 reviews — нарушение guidelines. Добавляйте рейтинг только при наличии verifiable reviews на странице.
Экранирование HTML в Answer.text. JSON-LD принимает plain text. Если ответ содержит <p>, используйте текст без тегов или strip HTML перед сериализацией:
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, '').trim();
}
Забытый logo размер. Google рекомендует logo минимум 112×112 px, форматы PNG/JPG/SVG. URL должен быть абсолютным.
Нужна помощь? Telegram → или vic.kell@ya.ru
FAQ
JSON-LD влияет на позиции в Google?
Напрямую — нет. Косвенно — да, через CTR: rich snippets занимают больше места в SERP и привлекают внимание. Разметка также помогает Google понять структуру страницы и связать entity (компания, услуга, FAQ).
Можно ли добавить JSON-LD в layout.tsx?
Да. Для глобальных схем (Organization, WebSite) — layout. Для страничных (FAQPage, Product) — конкретный page.tsx. Не дублируйте FAQPage на страницах без FAQ-блока.
Работает ли JSON-LD с Partial Prerendering?
Да. Server Component с JSON-LD попадает в статическую оболочку PPR. Робот получает schema в первом HTML-ответе, независимо от dynamic holes.
Нужен ли JSON-LD для Astro 5?
Да, подход тот же: <script type="application/ld+json"> в .astro-шаблоне с данными из Content Collections. Astro отдаёт чистый HTML — для SEO это даже проще, чем в SPA.
Сколько schema-типов на одной странице?
Google не ограничивает количество, но каждый тип должен быть релевантен контенту. Для лендинга услуг оптимально: Organization + WebSite + FAQPage + ProfessionalService — 4 сущности в одном @graph.
Заключение
JSON-LD в Next.js 15 — недорогой по трудозатратам способ улучшить представление лендинга в поиске. Server Components гарантируют, что разметка Schema.org приходит в HTML с первого байта. Связывайте Organization, FAQPage и Service через @graph и @id, синхронизируйте данные с Metadata API и видимым контентом, проверяйте через Rich Results Test.
Rich snippets не заменяют качественный контент и Core Web Vitals, но в конкурентной нише разработки сайтов FAQ-аккордеон в выдаче может стать решающим фактором клика. Настройте schema один раз — и мониторьте Enhancements в Search Console после индексации.