JSON-LD в Next.js 15: rich snippets для лендинга — KEL IT
Сайты 8 мин чтения

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Панель знаний, логотип, контактыЛюбой коммерческий сайт
WebSiteSitelinks search boxСайты с поиском
FAQPageРаскрывающиеся вопросы в SERPЛендинги с блоком FAQ
Product / ServiceЦена, рейтинг, availabilitySaaS, услуги с тарифами
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 и ошибки парсинга.

Чеклист после деплоя:

  1. View Source (не DevTools Elements) — убедитесь, что <script type="application/ld+json"> присутствует в исходном HTML
  2. Rich Results Test по production URL — 0 errors, warnings разберите отдельно
  3. Search Console → Enhancements — мониторинг FAQ, Breadcrumbs, Organization
  4. 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 после индексации.

KEL IT

Нужна разработка под ключ?

Я занимаюсь такими проектами профессионально. Telegram-боты, Mini Apps, сайты, мобильные и десктопные приложения. Расскажите о задаче — разберём и предложим решение.