Partial Prerendering в Next.js 15: SEO-лендинг — KEL IT
Сайты 8 мин чтения

Partial Prerendering в Next.js 15: SEO-лендинг с интерактивными блоками

Лендинг должен одновременно попадать в индекс Google за миллисекунды и показывать калькулятор цены, форму записи или анимированный hero-блок. Классический SSR отдаёт HTML на каждый запрос — это дорого на edge и не всегда нужно. Чистый SSG не даёт персонализации и динамики. Static Site Generation с client-side hydration — быстро, но JavaScript блокирует INP, а контент «пустой» до гидратации.

Partial Prerendering (PPR) в Next.js 15 решает этот компромисс: статическая оболочка страницы отдаётся мгновенно, а «дыры» для динамических компонентов заполняются потоком (streaming). Поисковый робот видит полный HTML с текстом и мета-тегами, пользователь — интерактивный интерфейс без просадки LCP и INP.

В этой статье соберём production-лендинг: App Router, shadcn/ui, framer-motion для hero-секции, JSON-LD для SEO и деплой на Vercel с включённым PPR.

Что такое PPR и чем он отличается от SSR и SSG

Partial Prerendering — экспериментальная, но уже применимая в продакшене функция Next.js 15. При сборке Next.js генерирует статический HTML-каркас страницы. Компоненты, помеченные как динамические (Suspense + dynamic() или cookies() / headers()), остаются «дырами» — placeholder’ами, которые заполняются при запросе через React Server Components streaming.

ПодходHTML для ботовИнтерактивностьTTFBСложность
SSGПолныйТолько client-sideМинимальныйНизкая
SSRПолныйПолнаяСреднийСредняя
PPRСтатическая оболочка + streamПолнаяМинимальныйСредняя

Для SEO-лендинга это означает: заголовки, тексты, FAQ, JSON-LD — в статике. Форма «Записаться», блок «Цена зависит от города», счётчик мест — в динамике. Google получает контент сразу, Core Web Vitals не страдают от тяжёлого JS на первом экране.

Включение PPR в next.config.ts:

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: {
    ppr: 'incremental',
  },
};

export default nextConfig;

Значение 'incremental' позволяет включать PPR постранично через export const experimental_ppr = true — не нужно мигрировать весь проект сразу.

Структура лендинга: статика и динамика

Типичный лендинг услуг делится на зоны с разной «температурой»:

Статические зоны (попадают в prerender):

  • Hero с заголовком, подзаголовком, CTA-текстом
  • Блок преимуществ, отзывы, FAQ
  • Footer с контактами и JSON-LD
  • Open Graph и meta-теги

Динамические зоны (streaming через Suspense):

  • Форма записи с валидацией (shadcn/ui)
  • Калькулятор стоимости
  • Блок «Свободные слоты на сегодня»
  • Персонализированный баннер по UTM-метке

Структура App Router:

app/
├── layout.tsx              # общий layout, шрифты, metadata
├── page.tsx                # лендинг с PPR
├── components/
│   ├── hero-static.tsx     # статический hero
│   ├── hero-animation.tsx  # framer-motion (client)
│   ├── booking-form.tsx    # динамическая форма
│   └── price-calculator.tsx
└── api/
    └── slots/route.ts      # API для слотов

Ключевой файл — page.tsx:

import { Suspense } from 'react';
import { HeroStatic } from '@/components/hero-static';
import { HeroAnimation } from '@/components/hero-animation';
import { BookingForm } from '@/components/booking-form';
import { PriceCalculator } from '@/components/price-calculator';
import { Benefits, FAQ, Footer } from '@/components/sections';

export const experimental_ppr = true;

export default function LandingPage() {
  return (
    <>
      <HeroStatic />
      <Suspense fallback={<div className="h-64 animate-pulse bg-muted" />}>
        <HeroAnimation />
      </Suspense>
      <Benefits />
      <Suspense fallback={<BookingFormSkeleton />}>
        <BookingForm />
      </Suspense>
      <Suspense fallback={<CalculatorSkeleton />}>
        <PriceCalculator />
      </Suspense>
      <FAQ />
      <Footer />
    </>
  );
}

HeroStatic — Server Component с текстом, который индексируется. HeroAnimation — Client Component с framer-motion, обёрнут в Suspense, чтобы не блокировать первый рендер.

Если нужна подобная разработка — напишите в Telegram.

SEO: metadata, JSON-LD и Core Web Vitals

PPR не отменяет базовую SEO-работу — он делает её эффективнее, потому что статический HTML отдаётся без ожидания динамики.

Metadata API

В Next.js 15 metadata объявляется в layout или page:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Разработка сайтов под ключ — Next.js, Astro, SEO',
  description: 'Лендинги и корпоративные сайты с Core Web Vitals 90+. Next.js 15, Astro 5, деплой на Vercel.',
  openGraph: {
    title: 'Разработка сайтов под ключ',
    description: 'Быстрые SEO-лендинги с Partial Prerendering',
    images: [{ url: '/og-landing.png', width: 1200, height: 630 }],
  },
  alternates: {
    canonical: 'https://example.com',
  },
};

Canonical и OG-теги попадают в статический HTML — робот видит их без JavaScript.

JSON-LD для LocalBusiness / Service

Structured data добавляем Server Component’ом:

export function JsonLd() {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'ProfessionalService',
    name: 'Kel IT — разработка сайтов',
    url: 'https://example.com',
    description: 'Лендинги и корпоративные сайты на Next.js и Astro',
    areaServed: 'RU',
    priceRange: '$$',
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Размещаем в layout.tsx — schema всегда в prerender, не зависит от клиентского JS.

Core Web Vitals: INP и LCP

INP (Interaction to Next Paint) стал ключевым метриком с 2024 года и остаётся критичным в 2026-м. PPR помогает:

  • LCP — hero-изображение и текст в статике, priority на <Image> из next/image
  • INP — тяжёлые client-компоненты (форма, калькулятор) загружаются после первого paint
  • CLS — skeleton’ы в Suspense fallback совпадают по размеру с финальным контентом

Для framer-motion в hero используйте LazyMotion и domAnimation — bundle framer-motion уменьшается в 10+ раз:

'use client';

import { LazyMotion, domAnimation, m } from 'framer-motion';

export function HeroAnimation() {
  return (
    <LazyMotion features={domAnimation}>
      <m.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5 }}
      >
        {/* декоративные элементы, не критичные для SEO */}
      </m.div>
    </LazyMotion>
  );
}

Динамические компоненты: форма и калькулятор

Форма записи — классический кандидат для динамической «дыры». Подключаем shadcn/ui, React Hook Form и Server Actions.

// components/booking-form.tsx
import { getAvailableSlots } from '@/lib/slots';

async function BookingFormInner() {
  const slots = await getAvailableSlots(); // fetch к API или БД

  return <BookingFormClient slots={slots} />;
}

export function BookingForm() {
  return (
    <Suspense fallback={<BookingFormSkeleton />}>
      <BookingFormInner />
    </Suspense>
  );
}

getAvailableSlots может читать cookies() для персонализации или обращаться к headless CMS — это автоматически делает компонент dynamic boundary.

Server Action для отправки:

'use server';

import { revalidatePath } from 'next/cache';

export async function submitBooking(formData: FormData) {
  const name = formData.get('name') as string;
  const slot = formData.get('slot') as string;

  // сохранение в CMS / CRM
  await saveBooking({ name, slot });
  revalidatePath('/');
}

Калькулятор цены — Client Component внутри Suspense. Логика расчёта на клиенте, начальные данные (тарифы) — из Server Component через props.

Деплой на Vercel и проверка PPR

Vercel — нативная платформа для Next.js 15. PPR работает на Edge Network без дополнительной настройки.

  1. Подключите репозиторий к Vercel
  2. Убедитесь, что experimental.ppr: 'incremental' в конфиге
  3. На страницах с PPR добавьте export const experimental_ppr = true
  4. Деплой — Vercel автоматически разделит static shell и dynamic holes

Проверка после деплоя:

curl -s https://your-site.vercel.app | head -100

В ответе должны быть статические секции (hero, FAQ) и placeholder’ы или streaming-разметка для Suspense-блоков.

Lighthouse / PageSpeed Insights:

  • LCP < 2.5s — статический hero
  • INP < 200ms — client-компоненты не блокируют main thread на первом экране
  • SEO 100 — metadata + JSON-LD в HTML

Google Search Console: через «Проверка URL» убедитесь, что rendered HTML содержит тексты из статических секций.

Для preview-окружений Vercel создаёт отдельные URL — тестируйте PPR на preview до merge в main.

Нужна помощь? Telegram → или vic.kell@ya.ru

FAQ

PPR стабилен для продакшена в 2026 году?

PPR помечен как experimental, но 'incremental' позволяет включать его точечно — только на лендингах, без риска для всего приложения. Vercel и крупные проекты уже используют PPR в production. Следите за changelog Next.js 16 — функция может стать stable.

Можно ли комбинировать PPR с ISR?

Да. Статическая оболочка может revalidate по revalidate или revalidatePath, а динамические holes обновляются при каждом запросе. Для лендинга с редко меняющимся контентом достаточно revalidate: 3600.

Нужен ли PPR, если лендинг полностью статичный?

Если нет форм, калькуляторов и персонализации — достаточно чистого SSG (Astro 5 или Next.js static export). PPR имеет смысл, когда есть хотя бы один динамический блок, а SEO-критичный контент должен быть в HTML сразу.

Как PPR влияет на headless CMS?

Контент из CMS (Contentful, Sanity, Strapi) можно загружать в статическую часть через generateStaticParams и build-time fetch. Динамические блоки — для данных, которые меняются чаще одного раза в час: слоты записи, остатки, A/B-тесты.

Чем PPR лучше React Server Components без PPR?

RSC без PPR рендерит всю страницу на сервере при каждом запросе (или кэширует целиком). PPR отделяет «холодный» статический контент от «горячей» динамики — TTFB ниже, CDN отдаёт оболочку из edge-кэша.

Заключение

Partial Prerendering в Next.js 15 — практичный инструмент для SEO-лендингов с интерактивными блоками. Статическая оболочка обеспечивает быстрый TTFB, полный HTML для поисковиков и высокие Core Web Vitals. Динамические «дыры» через Suspense дают формы, калькуляторы и персонализацию без компромисса по SEO.

Схема работает: experimental_ppr = true на странице, статика в Server Components, интерактив в Client Components внутри Suspense, JSON-LD и metadata в layout, деплой на Vercel. Для полностью статичных проектов по-прежнему рационален Astro 5 — но если стек уже Next.js, PPR закрывает типичный кейс «лендинг + одна динамическая фича» без перехода на полный SSR.

KEL IT

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

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