Nik McFly
ENRUKZISV
← Блог

Claude руководил, Codex писал код: два AI перенесли мой продакшен-сайт, пока я ответил на шесть вопросов

React SPA со сломанным SEO-слоем за 36 часов превратился в self-hosted сайт на Next.js: Claude руководил, а Codex написал все 7 803 строки кода.

#ai#codex#claude#nextjs#seo

React SPA с незаметно сломанным SEO-слоем за 36 часов и три продакшен-релиза переехал на self-hosted Next.js. Claude провёл аудит и ревью, ChatGPT Codex написал все 7 803 строки кода, а мой вклад свёлся к шести решениям и трижды сказанному слову «вперёд».


У меня небольшой музыкальный сайт. React 19, Vite, клиентский роутинг; изначально он был сгенерирован в Lovable, затем экспортирован на GitHub и перенесён на мой VPS. Утром 10 июля я попросил AI провести независимое архитектурное ревью. К вечеру 11 июля сайт уже работал в продакшене на Next.js: 45 канонических URL в Google, собственная аналитика с записью в Postgres, одношаговые редиректы со всех старых адресов и три rollback-файла, которые так и не понадобились.

Я не писал код. И не писал план, приёмочные тесты, конфиги Caddy или процедуры переключения. Всё это сделали две AI-системы, проверяя работу друг друга и порой доказывая, что другая ошиблась. Моя роль сократилась до «человека, который отвечает за необратимые решения».

Действующие лица

Я. Принимал решения о бренде — какой URL-slug получит EP, что делать со splash-страницей, — одобрял переключения продакшена и платил за сервер.

Claude — руководитель. Провёл первый аудит, составил план миграции, выдавал задания и независимо проверял заявления исполнителя на живом продакшене. Код приложения он не писал.

ChatGPT Codex — разработчик. Реализовал всё: приложение на Next.js, реестр маршрутов, приёмочные наборы, изменения Docker и Caddy, staging-окружения. Двадцать семь коммитов, 292 отслеживаемых файла, 7 803 физические строки исходного кода.

Такое разделение оказалось важнее, чем я ожидал. Не потому, что одному из AI нельзя доверять в человеческом смысле, а потому, что каждый замечал то, что пропустил другой. Причём в обе стороны.

Аудит Claude нашёл то, чего месяцами не видел никто

Известная проблема: одностраничные приложения отдают пустую HTML-оболочку, поэтому краулеры без JavaScript ничего не видят. Я ожидал, что аудит это подтвердит и предложит исправления.

Но он загрузил страницы в настоящем браузере, дождался завершения React и прочитал DOM:

document.title
// "Generic Site Title"   <- одинаковый на всех страницах
document.querySelectorAll('link[rel=canonical]').length
// 0

Для метаданных страниц сайт использовал react-helmet-async. В React 19 эта библиотека перестаёт применять head-теги и не выдаёт ошибку. Контент каждой страницы отображался нормально, но title, canonical и структурированные данные никуда не попадали. Единственным видимым симптомом была неизменная подпись вкладки браузера — а никто не разглядывает вкладки собственного сайта. Месяцы code review ничего не нашли, потому что код был правильным; умерла библиотека под ним.

Аудит также изучил актуальную документацию Google и обнаружил факт, убивший дешёвое решение: prerender-прокси с распознаванием ботов, к которому я бы прибегнул, Google официально признал устаревшим. А важные для меня AI-краулеры — GPTBot, ClaudeBot и PerplexityBot — вообще не исполняют JavaScript. Второго шанса показать метаданные нет. Значит, полная миграция.

Как выглядит задание от одного AI другому

Claude разбил миграцию на релизы и выдавал Codex задания с жёсткими границами. Самым интересным были не задачи, а ограничения:

  • Definition of done до начала кода. Пять проверок на страницу. Первая: метаданные и основной контент должны присутствовать в исходном HTML, что проверяется через curl без JavaScript. Для страниц песен это означало полный текст, проверенный построчно. Позже Codex назвал это самым полезным указанием: именно оно не позволило получить «Next.js только по названию» с клиентской загрузкой текстов и статей.
  • Реестр маршрутов как единственный источник правды. Один типизированный массив всех канонических URL. Из него строятся sitemap, robots.txt, llms.txt, карта редиректов и приёмочные тесты. Старый sitemap превратился в вымысел именно потому, что его поддерживали вручную.
  • Счётчик паритета, блокировавший переключение. Каждый маршрут начинал со статуса pending и должен был заслужить parity. Инструменты Codex не позволяли выпускать продакшен, пока счётчик не показал 25/25. В ранних отчётах было: «6 scaffold, 19 pending, 0 parity. Переключение невозможно».
  • Границы скоупа. Для каждого релиза было перечислено, чего нельзя касаться. Иначе набравший темп агент начнёт улучшать то, что вы не просили.

Эталон, кандидат, разница

Для визуальной проверки Codex сделал трёхпанельные сравнения каждой страницы: живой продакшен слева, кандидат на Next.js в центре и пиксельная разница справа — для десктопа и ширины 375 px. За весь проект в репозиторий попало 101 такое изображение. Листать стену почти чёрных diff-панелей — ближайший аналог детектора лжи для этой миграции: любое смещение макета светится контуром, с которым не поспоришь.

Функциональная часть была ещё строже. HTTP acceptance-скрипт содержит 72 статических проверки. На 45 маршрутах, с QR-параметрами, метаданными, JSON-LD, OG-изображениями и приватными поверхностями, они разворачиваются в 1 505 выполненных проверок на каждый продакшен-релиз.

Мои любимые двенадцать строк проекта превращают архитектурное правило в условие, способное уронить сборку. У каждого редиректа должен быть ровно один владелец: либо reverse proxy, либо фреймворк — никогда оба:

const forbiddenCaddyTokens = [
  "redir /about ",
  "redir /home ",
  "redir /blog ",
  "path_regexp legacyPath",
  "redir /legacy-page ",
  "redir /old-campaign ",
];

for (const token of forbiddenCaddyTokens) {
  assert.equal(caddy.includes(token), false,
    `Path redirect still has two owners: ${token.trim()}`);
}

assert.equal(caddy.includes("/legacy-resolver/"), false,
  "Caddy must not own this route after the Next resolver ships");

Редирект, определённый в двух местах, однажды обязательно начнёт противоречить сам себе. Теперь об этом говорит CI.

Где ошибался каждый AI — официально

Ошибки Codex были процессными. Трижды он сохранял отчёты с доказательствами в /tmp, который исчезает после перезагрузки, хотя ему велели коммитить их. Четыре отчёта подряд содержали пустые секции скриншотов и одновременно утверждали, что визуальный паритет проверен. Auth-тесты он запустил на продакшен-базе, потому что именно там был доступный API. Каждую проблему исправили, превратив требование в merge blocker; в поздних отчётах Codex перечислял все созданные им QA-записи в продакшене с их ID.

Ошибки Claude были ошибками уверенности. Он получил production sitemap из устаревшего кэша и объявил уже развёрнутое исправление отсутствующим; Codex предъявил хэши контента и выиграл спор. Claude заявил, что backend-репозиторий существует, потому что на него указывал git remote, но репозиторий никогда не создавали. Баг рассылки он приписал feature-ветке Codex; Codex доказал, что ветка не касалась этого кода, и нашёл настоящую причину: Drizzle завернул ошибку PostgreSQL о дублирующемся ключе в error.cause, где обработчик её не видел.

Когда Codex спросили, с чем он был не согласен, но промолчал, ответ оказался неожиданным. Он считал pixel diff каждой юридической страницы и логина избыточным — и был прав. Не верил, что один прогон Lighthouse должен быть жёстким гейтом — тоже прав, хотя гейт нашёл реальные проблемы prefetch и загрузки изображений. И считал llms.txt экспериментальной гигиеной, а не доказанным фактором ранжирования — снова прав, но при цене в один сгенерированный файл сделать его всё равно стоило. Мой вывод: исполнитель, который молча выполняет сомнительные для него инструкции, — дефект процесса, а не достоинство. Спрашивайте о сомнениях.

Худший момент мы создали сами

Не DNS и не отладчики social preview. При подготовке Phase 1C нативные Docker-сборки накопили промежуточные слои, пока диск VPS не заполнился на 100%. PostgreSQL не смог записать checkpoint и вошёл в цикл восстановления, а production API ненадолго начал отвечать 500. Staging-упражнение вызвало небольшой, но настоящий продакшен-инцидент.

Ответ Codex был образцовым: остановить готовящуюся миграцию и переключение Caddy, удалить только висячие build-слои, дать Postgres самостоятельно завершить WAL recovery без перезапуска контейнера и без касания файлов данных, затем заново всё проверить. Свободное место выросло с нуля до 15,79 ГБ, в операционный план добавили еженедельную очистку образов, а у каждой будущей сборки появилось предварительное условие: 15 ГБ свободного места до загрузки образов. Инцидент занял двадцать минут и породил самое полезное эксплуатационное правило проекта.

Цифры

Метрика До После
Маршруты с уникальными метаданными в исходном HTML 0 45
Самый большой JS при первой загрузке 1,3 МБ 658 КБ
Mobile LCP страницы песни не измерялся 1,8 с
Неизвестные пути, возвращающие настоящий 404 0% 100%
Реально существующие URL в sitemap 9 из 15 45 из 45
Выполненные acceptance-проверки на релиз 0 1 505
Закоммиченные visual-diff изображения 0 101
Переключения продакшена / потребовавшиеся откаты 3 / 0
Написанные мной строки кода 0 0

По git timestamps блоки реализации занимали от 25 до 55 минут, staging acceptance — около 90 минут, а каждое окно переключения продакшена — примерно 15 минут. Само переключение трафика занимало 302 миллисекунды. Весь путь от первого аудита до третьего продакшен-релиза уложился в 36 часов.

Что на самом деле сделал я

Шесть решений: подтвердил целевую архитектуру, выбрал URL-slug для EP, решил судьбу splash-страницы, определил подход к аналитике, принял компромисс по контентному долгу и разрешил три переключения продакшена. Каждое было бизнес-решением или разрешением на необратимое действие. Ни одно не требовало открыть редактор.

Теперь я готов защищать такое разделение труда: AI отвечают за обратимую работу и проверку, люди — за необратимые решения и стандарты. Стандарты — тоже настоящая работа, и пришли они со стороны ревью: definition of done, границы скоупа, правило хранить артефакты в git, отрепетированные откаты. Без этого слоя я, вероятно, получил бы быструю миграцию с тихими пробелами, а не быструю миграцию с 1 505 доказательствами.

Если захотите повторить: дайте агенту-исполнителю реестр, из которого всё выводится, вместо набора файлов для ручной синхронизации; заставьте подтверждать заявления закоммиченными артефактами; разрешите ему самому блокировать переключение; спросите, с чем он втайне не согласен; и оставьте одно слово за собой.

Моим было «вперёд».