Маркетинговый сайт Testlify насчитывает более 11 000 страниц. Прежняя конфигурация на WordPress создавала накладные расходы на серверный рендеринг и узкие места из-за плагинов при каждом запросе, что влияло на скорость загрузки страниц и позиции в поисковых системах. Я возглавил архитектурное решение мигрировать сайт на Next.js 16, и развёртывание сейчас продолжается.
Архитектурное решение
- Кеш-ориентированный подход вместо WordPress: страницы рендерятся один раз при первом запросе и кешируются бессрочно, вместо обращения к базе данных при каждом запросе или многочасовой пакетной пересборки для 11 000+ страниц. Вебхук Payload CMS сбрасывает кеш только для тех страниц, которые реально затронуты изменением контента.
- Интеграция с headless CMS: контент управляется через Payload CMS, самостоятельно размещаемую headless CMS на базе Postgres, позволяя маркетинговым командам редактировать и публиковать страницы, пока хуки инвалидации кеша автоматически подхватывают каждое изменение.
- Миграция в масштабе: спланирована и выполнена поэтапная миграция 11 000+ страниц, проверенная постранично против живой карты сайта продакшена, с сохранением структуры URL, канонических тегов и существующего SEO-авторитета по всем категориям.
Почему это важно
Перевод сайта из 11 000+ страниц с WordPress на кеш-ориентированную архитектуру сокращает время загрузки страниц, улучшает техническую SEO-производительность и позволяет маркетинговым командам публиковать обновления без привязки к инженерным деплоям.
Ответственность, работа всё ещё продолжается
Я возглавил архитектурное решение уйти с WordPress. Это не сольная разработка: более 2 500 коммитов примерно от 15-20 участников с июля 2026 года, активность сохраняется вплоть до этого месяца, а на момент последней проверки статуса единственным оставшимся шагом был перевод DNS на продакшен.
Почему это не классический Jamstack
Очевидный подход для 11 000+ страниц: статическая генерация во время сборки. Он здесь не подходит: полная пересборка по всем локалям и типам контента заняла бы часы, а одно изменение контента не должно требовать пересборки всего сайта, чтобы выйти в продакшен. Вместо этого каждый динамический маршрут (category, author, hr-tools, hr-guide, blog, test-library и другие) следует одной и той же схеме: сборка предварительно рендерит ровно одну образцовую страницу на тип маршрута, поскольку Cache Components требует хотя бы один параметр для сборки, а сама сборка должна выполняться без единого запроса к базе данных. Каждая другая страница под этим маршрутом рендерится при первом реальном запросе под Cache Components в Next.js 16, а затем остаётся кешированной бессрочно (cacheLife установлен в собственную константу Next INFINITE_CACHE), пока хук afterChange в Payload CMS не вызовет подписанный эндпоинт ревалидации для этой конкретной страницы. Изменения контента распространяются за секунды, а не за полный цикл пересборки, при этом кеш со стороны посетителя по-прежнему ведёт себя как edge-кеш CDN. Сам эндпоинт ревалидации подписан HMAC и проверяется сравнением за постоянное время, и намеренно ограничен инвалидацией только одного тега за раз: только отдельный, вручную запускаемый административный путь может очистить тег для всей коллекции, так что утёкший секрет вебхука может в худшем случае инвалидировать одну страницу, а не обрушить кеш всего сайта разом.
Два бага фреймворка, найденные в продакшене и исправленные у источника
Во время сборки возникли две отдельные проблемы Next.js 16, обе теперь задокументированы прямо в next.config.ts, чтобы следующий человек не открывал их заново на своём опыте. Во-первых, cacheComponents по умолчанию незаметно включает enablePrerenderSourceMaps, что убивало продакшен-сборку по памяти во время статической генерации страниц; проблема была прослежена до исходного кода самого Next и исправлена явным переопределением. Во-вторых, использование Infinity для времени жизни кеша сериализуется в JSON как null на пути к воркерам сборки Next, что незаметно понижает этот маршрут до некешируемого вместо выдачи ошибки, ломая пререндеринг во всех локалях, пока это не будет обнаружено; исправлено использованием собственной константы Next INFINITE_CACHE вместо сырого Infinity.
Исправление SEO для граничного случая стриминга
При стриминге в Cache Components автоматический notFound() в Next не может зафиксировать HTTP-статус 404 до того, как оболочка страницы уже начала стримиться клиенту, поэтому краулер, читающий только эту оболочку, никогда его не увидит. Исправление вручную вставляет директиву noindex в первый флаш, чтобы краулер, остановившийся на оболочке, всё равно получил правильный сигнал ещё до того, как остальная часть страницы разрешится.
Модель данных и масштаб
- 11 000+ страниц формируются из нескольких типов контента (category, author, hr-tools, hr-guide, leadership, test-library), пересекающихся с каждой поддерживаемой локалью через
next-intl, а не из одного плоского источника контента. - Каждый продакшен-URL WordPress проверяется против вывода этого проекта в отдельном аудите (
docs/audits/url-parity-with-production.md): URL, который сегодня отдаёт WordPress, а этот проект возвращает 404, это потерянный трафик, а не приемлемый пробел. - Юнит-тесты (Vitest) покрывают генерацию карты сайта, откат к сид-данным каталога и ротацию связанного контента; Playwright покрывает сквозные сценарии.
Build vs. Buy
Самостоятельно размещаемая Payload CMS на Postgres вместо SaaS headless CMS: сохраняет владение контентом и базой данных внутри компании, вместо оплаты SaaS-подписки за место для маркетингового сайта. Собственные Cache Components и инвалидация по тегам Next.js вместо самописного слоя CDN/ISR: поведение кеширования обеспечивается фреймворком, а не инфраструктурой, которую этой команде иначе пришлось бы строить и эксплуатировать самой.
Стек
Next.js 16, React, TypeScript, Payload CMS на Postgres, next-intl для локализации, Vitest для юнит-тестов, Playwright для сквозных тестов, развёрнуто на Vercel.