SEO для сайтов на React: что нужно сделать, чтобы вас нашли
Сайт на React может быть невидим для поиска, даже если выглядит отлично. Разбираем причины и полный набор технических мер, которые это исправляют.

Регулярная ситуация: заказчик получил современный сайт на React, а через полгода обнаружил, что в поиске его нет. Причина техническая и вполне решаемая, но исправлять её после запуска дороже, чем учесть на старте.
Почему одностраничное приложение плохо индексируется
В классическом одностраничном приложении сервер отдаёт почти пустой документ, а содержимое собирает JavaScript в браузере. Поисковый робот умеет выполнять скрипты, но делает это с задержкой и не всегда полностью. В результате страница попадает в индекс без текста либо не попадает вовсе.
<!-- Ответ сервера: содержимого нет -->
<div id="root"></div>
<script src="/assets/app.js"></script>
<!-- Ответ при серверном рендеринге: текст есть сразу -->
<main>
<h1>Разработка складского учёта</h1>
<p>Автоматизируем приёмку, отгрузку и инвентаризацию…</p>
</main>Серверный рендеринг как основа
Решение — отдавать готовый HTML с сервера. В Next.js это поведение по умолчанию: компоненты выполняются на сервере, и в браузер уходит документ с текстом. Важно не потерять это свойство, безосновательно помечая страницы как клиентские.
- Директиву клиентского компонента ставьте только там, где нужны состояние и обработчики событий.
- Выносите интерактивные части в отдельные небольшие компоненты, а текстовое содержимое оставляйте серверным.
- Не загружайте основной текст страницы запросом из браузера: он должен быть в ответе сервера.
Метаданные для каждой страницы
Один общий заголовок на весь сайт — распространённая ошибка. Каждая страница должна иметь собственный заголовок и описание, отражающие её содержимое.
import type { Metadata } from "next"
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const service = getService(slug)
if (!service) return { title: "Страница не найдена" }
return {
title: service.metaTitle ?? service.title,
// 140–165 символов: столько показывает поиск в сниппете
description: service.description,
alternates: { canonical: `/uslugi/${service.slug}` },
openGraph: {
title: service.title,
description: service.description,
type: "article",
images: [{ url: service.cover, width: 1200, height: 630 }],
},
}
}Карта сайта и файл robots
Карту сайта нужно генерировать из тех же данных, что и страницы. Написанная руками, она устаревает после первого добавления раздела и начинает вводить робота в заблуждение.
import type { MetadataRoute } from "next"
import { services } from "@/lib/services"
import { posts } from "@/lib/blog"
export default function sitemap(): MetadataRoute.Sitemap {
const base = "https://example.ru"
return [
{ url: base, changeFrequency: "weekly", priority: 1 },
...services.map((service) => ({
url: `${base}/uslugi/${service.slug}`,
changeFrequency: "monthly" as const,
priority: 0.8,
})),
...posts.map((post) => ({
url: `${base}/blog/${post.slug}`,
// Дата изменения помогает роботу переобойти обновлённые страницы
lastModified: post.updated ?? post.date,
priority: 0.6,
})),
]
}Микроразметка
Структурированные данные не поднимают позиции напрямую, но дают расширенный сниппет: хлебные крошки в выдаче, блок вопросов и ответов, данные организации. На практике это заметно повышает долю переходов.
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faq.map((item) => ({
"@type": "Question",
name: item.q,
acceptedAnswer: { "@type": "Answer", text: item.a },
})),
}
// Разметка обязана соответствовать видимому тексту:
// расхождение считается нарушением
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>Технический чек-лист
- Текст страницы присутствует в исходном коде ответа сервера.
- У каждой страницы уникальные заголовок и описание.
- Настроен канонический адрес, дубли страниц с параметрами не индексируются.
- Карта сайта генерируется автоматически и содержит даты изменения.
- Заголовки образуют иерархию: один H1 на страницу, дальше H2 и H3 по смыслу.
- Внутренние ссылки ведут на связанные страницы обычными ссылками, а не обработчиками нажатия.
- Настроена микроразметка организации, хлебных крошек и статей.
- Показатели Core Web Vitals в зелёной зоне на мобильных устройствах.
Техническое SEO не приносит трафик само по себе. Оно убирает препятствия, из-за которых хороший контент не доходит до выдачи.
Частые вопросы
- Почему сайт на React не видно в поиске?
- В классическом одностраничном приложении сервер отдаёт пустой документ, а содержимое собирает JavaScript в браузере. Робот выполняет скрипты с задержкой и не всегда полностью, поэтому страница попадает в индекс без текста. Проверить просто: откройте исходный код страницы — если текста в нём нет, робот его тоже не увидит.
- Решает ли Next.js проблему индексации автоматически?
- В значительной степени да: серверный рендеринг здесь поведение по умолчанию. Но свойство легко потерять, помечая страницы клиентскими без необходимости или загружая основной текст запросом из браузера. Интерактивные части следует выносить в отдельные небольшие компоненты, оставляя текст серверным.
- Нужна ли микроразметка и что она даёт?
- Структурированные данные не поднимают позиции напрямую, но обеспечивают расширенный сниппет: хлебные крошки в выдаче, блок вопросов и ответов, данные организации. Это заметно повышает долю переходов. Важно, чтобы разметка соответствовала видимому на странице тексту.
- Как правильно сделать карту сайта?
- Генерировать её из тех же данных, что и страницы, включая даты последнего изменения — так робот быстрее переобходит обновлённые разделы. Карта, написанная вручную, устаревает после первого добавления раздела и начинает вводить робота в заблуждение.
Похожая задача в вашем проекте?
Разберём вашу ситуацию и пришлём оценку по этапам — без обязательств и общих слов.
Обсудить задачу
