Headless CMS давно перестала быть экзотикой для корпоративных блогов и медиа, но вокруг неё держится красивое заблуждение: раз контент отдаётся через API, поисковая оптимизация прилагается к архитектуре бесплатно. Ничего подобного. Связка из Strapi или Sanity на бэкенде и Next.js на фронте не даёт сайту ни одного очка в выдаче, она лишь возвращает разработчику контроль над каждым техническим слоем. SEO здесь не включённая опция, а работа: рендеринг, карты адресов, канонические ссылки, разметка и обновление кэша проектируются руками. Ниже рабочая архитектура такой связки по слоям, с примерами кода и разбором типовых ошибок команд, которые обходятся дешевле часа работы, но стоят проекту месяцев трафика.
Чем headless архитектура отличается от классической CMS для SEO
В классической связке контент и страница живут в одной системе: движок хранит запись и сам отвечает за её разметку, адреса и карту сайта. В headless связке хранение и отдача разнесены: Strapi или Sanity хранят контент и отдают его по API, Next.js запрашивает данные и собирает страницу. Поисковая система видит только результат сборки, и вся техническая часть переезжает из админки в код фронтенда.
Из этого следует главное. Плюс: мета-теги, канонические адреса, структурированные данные и скорость рендеринга программируются точно под задачу, без ограничений плагинов и шаблонов, и один и тот же контент кормит сайт, приложение и любые будущие каналы. Минус: всё перечисленное кто-то должен написать, и по умолчанию не написано ничего. Команда, переехавшая на headless без отдельной задачи по SEO в бэклоге, получает красивую систему, которая молча теряет трафик: без canonical, без карты сайта, с пустыми title.
Свобода проявляется в мелочах, которых не видно в демо. Логика канонических адресов для страниц фильтров собирается своими правилами, а не компромиссом чужого плагина. Посадочные разделы под группы запросов собираются из повторяемых блоков контент-модели без программиста. Перелинковка по смыслу вроде похожих публикаций берётся из полей API и не требует ручного проставления ссылок. Каждая из этих мелочей на конструкторе отдельная заказная доработка, в headless они умещаются в обычную работу фронтенда.
За гибкость платят дисциплиной, и это не фигура речи, а строчка в смете работ.
Strapi или Sanity выбор системы хранения под тип проекта
Обе системы зрелые, но устроены по-разному, и выбор между ними это выбор модели владения. Strapi 5 это открытый фреймворк на Node.js под лицензией MIT: проект разворачивается командой npx create-strapi-app@latest на собственном сервере, типы контента собираются визуальным конструктором, наружу отдаются REST и GraphQL API, из коробки работают черновики, роли, вебхуки и локализация. Данные и код лежат у владельца, панель администратора кастомизируется кодом, любой нестандартный сценарий решается своим контроллером. Плата за свободу - администрирование: обновления, резервные копии и безопасность остаются на команде.
Sanity работает иначе: контент хранится в управляемом Content Lake, студия это открытая React-панель, которую редактируют кодом и подстраивают под редактора до отдельной кнопки, данные запрашиваются языком GROQ прямо из фронтенда, изображения обрабатываются на лету CDN платформы. Работают совместное редактирование в реальном времени и релизы контента, когда выпуск публикации собирается заранее и выходит по кнопке. Плата - хранение у вендора и привязка к его инфраструктуре.
В обеих системах контент-модель проектируется под поисковую задачу с первого дня: у каждого типа записи заводятся поля seoTitle, seoDescription и ogImage, редактор видит их рядом с текстом, а не где-то в скрытых настройках. Черновики и публикация по кнопке позволяют держать материал в работе неделями без риска утечки полуготовой страницы в индекс. В Strapi языковые версии это отдельные записи, в Sanity переводы живут в одном документе, но карта hreflang в обоих случаях генерируется на фронтенде одинаково.
Проектам с требованием держать данные в своей инфраструктуре ближе Strapi, распределённой редакции с быстрым темпом публикаций ближе Sanity.
Рендеринг страниц на Next.js от статической генерации до ISR
Именно здесь решается судьба индексации. Next.js 16 с App Router и React Server Components рендерит страницы на сервере заранее или в момент запроса, и робот получает готовый HTML, не дожидаясь выполнения клиентского JavaScript. Адреса страниц собираются из данных CMS функцией generateStaticParams, типовой пример для раздела публикаций выглядит так:
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
Для контента, который обновляется без релизов, есть инкрементальная регенерация: страница генерируется один раз и пересобирается не чаще заданного интервала. Интервал объявляется одной строкой в файле маршрута:
export const revalidate = 3600
Час кэша означает, что правка из CMS появится на сайте максимум через час. Когда важна мгновенность, webhook из Strapi или Sanity вызывает служебный маршрут Next.js, тот выполняет revalidateTag и сбрасывает кэш только затронутых страниц. Посетитель и робот видят одну версию страницы, расхождений между сниппетом в выдаче и живым текстом не возникает.
Режим рендеринга выбирается постранично, и это главный подарок архитектуры. Каталог с ценами и остатками рендерится в момент запроса, чтобы робот и посетитель видели актуальное состояние. Публикации собираются статикой на этапе сборки и пересобираются по расписанию или вебхуку. Поиск по сайту и личные кабинеты остаются клиентскими, индексация им не нужна, и они честно закрыты от робота. Один проект обслуживает три режима сразу, и за каждый режим отвечает код, а не надежда.
Ошибочный адрес закрывается функцией notFound, и сервер отвечает честным статусом 404, а не мягкой заглушкой с картинкой.
Технический слой SEO карта сайта robots редиректы и canonical
Карту сайта в Next.js не пишут руками: файл app/sitemap.ts это служебный маршрут, который собирается из данных CMS и кэшируется по умолчанию. Строки карты берутся из того же API, что и страницы, поэтому рассинхрона между сайтом и картой не бывает:
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts()
return posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
}))
}
Рядом живёт app/robots.ts с правилами обхода и ссылкой на карту:
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: 'https://example.com/sitemap.xml',
}
}
Постоянные редиректы описываются в конфигурации проекта массивом пар из старого адреса и нового. Служебные маршруты api и превью закрываются Disallow в том же файле, а хвост слэша задаётся одним флагом trailingSlash сразу для всего проекта. Маршрутизатор гарантирует единственность каждого адреса, если команда не наплодит дубли сама, и половина технического SEO в headless это дисциплина адресов. Мета-теги страницы отдаёт функция generateMetadata, и в неё же встраивается канонический адрес:
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.seoTitle,
description: post.seoDescription,
alternates: { canonical: `https://example.com/blog/${params.slug}` },
}
}
Языковые версии сшиваются в том же объекте полем languages в alternates, и hreflang генерируется для всех локалей разом. Ни одной из перечисленных вещей нет в коробке Strapi или Sanity, всё это код фронтенда, и писать его нужно до релиза, а не после первого аудита.
Структурированные данные и Open Graph из полей контентной модели
Разметка в headless связке строится из тех же полей API, что и сама страница. В схему статьи попадают заголовок, дата, автор и обложка из полей Strapi, JSON-LD сериализуется на сервере и вставляется в layout одним скриптом. Расхождение между видимым текстом и разметкой здесь исключено конструктивно: источник один, валидатор Schema.org видит ровно то, что собрано из полей, а правка в CMS обновляет и страницу, и сниппет одновременно.
Open Graph в такой связке сильнее, чем в классических движках: Next.js генерирует превью-картинки на лету через ImageResponse, подставляя в шаблон заголовок и автора, и каждая статья получает свою карточку без дизайнера. Вес изображений держит next/image: конвертация в современные форматы, точные размеры под вьюпорт и ленивая загрузка работают из коробки, CDN хостинга отдаёт файлы с ближайшего узла. Шрифты подключаются через next/font и лежат в сборке: без внешних запросов, без мигания текста при отрисовке, без лишнего сдвига макета в метриках.
Зрелые команды встраивают проверку разметки в конвейер сборки: перед деплоем сериализованный JSON-LD каждой страницы проходит валидацию, и сборка падает, если схема потеряла обязательное поле или собрала дубль. Стоит такая проверка копейки, а ловит целый класс ошибок, который на классических движках находят на живом сайте через месяц после релиза.
Типичные ошибки команд при переходе на headless связку
Практика аудитов таких проектов складывается в короткий список повторяющихся ошибок. Самые дорогие из них выглядят так:
- Контент запрашивается на клиенте в useEffect, и робот получает каркас страницы без текста;
- Превью-деплои и черновые версии открываются по индексируемым адресам без тега noindex;
- Кэш инкрементальной регенерации не сбрасывается вебхуком, и в выдаче висит устаревшая цена;
- Адреса собираются из слагов без транслитерации и контроля дублей, canonical не проставлен;
- В карте сайта нет lastModified, в разметке статей нет дат, и робот не видит свежесть контента;
- Серверный рендеринг всего сайта на слабом хостинге добавляет секунды к TTFB.
Каждый пункт лечится парой строк кода, поэтому главный вопрос на старте - заложенный в смету час работы разработчика на технический слой, а не героические исправления после падения трафика. Зрелые хостинги закрывают превью-домены заголовком x-robots-tag noindex автоматически, но полагаться на чужую заботу в вопросе индексации не стоит: собственный запрет в метаданных виден и в аудите, и в панели поисковика.
Отдельная беда - адреса со слэшем на конце и без него, отдающие одинаковый контент со статусом 200. Поведение задаётся флагом trailingSlash на весь проект, но если половина ссылок собрана с хвостом, а половина без, canonical рассинхронизируется с адресной строкой и робот начинает видеть удвоенный сайт. Проверяется это за минуту краулером, а всплывает обычно в отчётах индексации, когда адресов в индексе оказывается вдвое больше ожидаемого.
Проекты ради которых стоит собирать связку Strapi или Sanity с Next.js
Headless не нужен витрине из пяти страниц: ради лендинга тянуть Strapi, Next.js и конвейер сборки не станет даже энтузиаст. Архитектура окупается там, где контент обновляется чаще релизов, страниц сотни и тысячи, редакция работает параллельно с разработкой, а посадочные разделы собираются из повторяемых блоков без программиста. Медиа, корпоративные блоги, документация, магазины с крупным каталогом и несколькими рынками - типичные жители этой связки.
Честности ради: связка требует разработчика в штате или на подряде. Правки контента редактор делает сам, но любое изменение макета, разметки или адресной логики это задача для разработчика. Проекту без технического ресурса headless обойдётся дороже конструктора на каждой мелочи. Проект с ресурсом получает систему, где рост объёма контента не требует пропорционального роста рук.
Рабочее правило напоследок: чем сложнее контентная модель, тем выгоднее headless, чем проще проект, тем дороже он обойдётся. Strapi или Sanity выбирайте по требованиям к хранению данных, Next.js по нагрузке и режиму рендеринга, а карту сайта, canonical, разметку и метрики Core Web Vitals закладывайте в первый спринт. Тогда архитектура отработает своё: скорость, контроль и масштабируемость вместо надежды, что поисковая оптимизация случится сама.