Введите минимум 3 символа для поиска

·10 мин чтения

Как мы перевели The Structure на Laravel, не изменив ни одного байта

A drawing of the six review layers of The Structure on a light table, with a sheet of tracing paper lifted from it, a red check mark and a red pencil

The Structure проверяет технологии компаний перед сделкой. Когда его собственный сайт в первые дни после запуска пришлось перевести со статической сборки на Laravel, мы провели переезд по тому же стандарту, которого бренд требует от других: каждое утверждение подтверждено доказательством, а для читателей и поисковиков не изменилось ничего.

The Structure это бренд технического дью-дилидженс для сделок с софтверными компаниями, который мы выделили из PERETZ. Он читает код, инфраструктуру, данные и права собственности компании для тех, кто её покупает или продаёт, и от каждой проверяемой системы требует одного: покажите доказательства. Эта статья о первой системе, которую ему пришлось проверить, о его собственном сайте.

Сайт запустился как статическая сборка: генератор на Python записывал пятьдесят HTML-файлов, карту сайта и поисковый индекс, а сервер просто отдавал их. Так получилось быстрее всего выйти в сеть. Через несколько дней сайт нужно было перевести на Laravel, чтобы команда вела его так же, как мы ведём peretz.agency: с админкой для статей, авторов, страниц и заявок и с одной панелью для поиска и аналитики.

У переезда было одно правило. Его не должен заметить никто: ни люди, которые читают сайт, ни поисковые системы, которые только начали на него приходить. Поэтому целью стал сайт, который отдаёт те же байты, что и раньше, страница за страницей, и путь назад, который занимает одну команду. Статью написала Peretz Group, а в этом проекте это основатель, который задавал правила и принимал решения, и Claude, ИИ, который делал техническую работу под его контролем.

Зачем

Переезд, которого никто не должен заметить

Новый сайт уязвимее всего в первые недели после запуска. Поисковые системы как раз решают, что это за сайт, какие страницы оставить и как часто возвращаться. Руководство Google о переезде без смены адресов считает смену инфраструктуры, на которой работает сайт, отдельным переездом и говорит, что если Googlebot не встречает серьёзных проблем или замедлений, он будет обходить сайт так быстро, как нужно. Новый фреймворк под теми же адресами и есть такой переезд, а проблему легко добавить нечаянно: переставленный атрибут, другой формат даты, новая cookie, страница, которая отвечает чуть медленнее.

Была и вторая причина, ближе к самому бренду. The Structure объясняет основателям, что покупатель проверит каждое утверждение в их документе о продаже, а утверждение без доказательства превращается в вопрос посреди эксклюзивности. Странно было бы говорить это клиентам и переносить собственный сайт на ощущении, что всё вроде бы выглядит нормально.

Мы перенесли свой сайт так, как просим продавцов доказывать утверждения.

Стандарт

Что значило «то же самое»

Ещё до первой строки кода мы определили «то же самое» так, чтобы это мог проверить скрипт. Каждый публичный адрес отрисовывается новым приложением и сравнивается побайтно с файлом, который для него записала статическая сборка. Это пятьдесят HTML-файлов, карта сайта со всеми 33 адресами, robots.txt, страница 404 и поисковый индекс, который читает в браузере поиск по сайту.

Сравнение работает как команда artisan. Она собирает запрос для каждого адреса, пропускает его через HTTP-ядро приложения, берёт ответ и сравнивает его со статическим файлом на диске. Если страница отличается, команда печатает первые отличающиеся строки, старую над новой, так что отличие становится строкой, которую можно прочитать, а не догадкой. Команду запускали после каждого изменения, и переезд ждал, пока она не сообщит, что все страницы совпадают.

Одно отличие допустили сознательно. В статической карте сайта у каждой страницы стояла дата последней сборки, то есть день, когда запускался генератор, а не день, когда что-то изменилось. Версия на Laravel даёт каждой странице дату её настоящей последней правки: дату статьи или её обновления, дату правки в админке для страниц и авторов и фиксированную базовую дату для страниц, которые с запуска никто не трогал. Это единственное место, где новый сайт говорит точнее старого, поэтому проверка сравнивает адреса в карте сайта, а даты оставляет в стороне.

Детали

Где Python и PHP расходятся

Статический генератор занимал около 2 700 строк на Python в двенадцати файлах. Новое приложение это около 3 500 строк PHP, маршрутов и шаблонов. Большая часть переноса была обычной работой. Интересными оказались мелкие места, где два языка по-разному отвечают на, казалось бы, один и тот же вопрос, и где один неверный символ проявился бы на каждой странице.

ЧтоPythonPHP по умолчанию
Округление половиныround(2.5) даёт 2: половина идёт к ближайшему чётномуround(2.5) даёт 3: половина идёт от нуля
Деление по пробеламstr.split() считает пробелами неразрывные пробелы и старые управляющие разделителиобычный \s в регулярном выражении PHP часть из них пропускает
Запись JSONjson.dumps ставит пробел после каждой запятой и двоеточияjson_encode пишет без пробелов и экранирует каждый слеш
Экранирование кавычекhtml.escape превращает апостроф в 'htmlspecialchars пишет '
Даты"%b %-d, %Y" даёт Nov 9, 2026для той же даты нужен формат 'M j, Y'

Время чтения каждой статьи считается по словам и округляется, так что правило округления решает, будет на карточке десять минут или одиннадцать. Структурированные данные записаны внутри страницы в виде JSON, поэтому пробелы после запятых тоже часть байтов. Заголовки с апострофами попадают в метатеги, так что последовательность экранирования важна в каждой статье, где он есть.

Решением стал небольшой вспомогательный класс примерно на сто строк, который делает то же, что делали функции Python: так же округляет, так же понимает пробел, так же раскладывает JSON, так же экранирует и так же пишет даты. Каждый шаблон использует его там, где генератор использовал Python, а сравнение доказало это страница за страницей.

Контент

Те же страницы, теперь с админкой

Когда страницы совпали, контент переехал из файлов Python в базу данных, а у сайта появилась админка в том же виде, что и админка PERETZ, только на английском и в цветах The Structure. Статьи пишутся в визуальном редакторе вместе с FAQ и максимум двумя авторами, а при сохранении админка предупреждает о наших правилах, например о длинных тире, слишком коротком тексте или FAQ меньше чем из трёх вопросов. У авторов, страниц, заявок, пользователей и настроек свои разделы.

Главная страница админки это панель: заявки, контент в цифрах, какие поисковые и ИИ-краулеры приходили на сайт и, по мере того как каждый сервис даёт доступ, данные поиска и аналитики, которые приносит ночная задача. Она повторяет подход аудита индексации, который мы сделали для peretz.agency.

Публичные страницы сохранили поведение статического сайта. Они одинаковы для каждого посетителя и, как и раньше, не ставят cookies; сессии есть только внутри админки. Когда редактор сохраняет страницу, сайт переписывает свой поисковый индекс, и поиск у посетителя сразу видит изменение.

Переключение

Одна команда, и путь назад встроен

Само переключение было одним скриптом, и большая его часть о том, что будет, если что-то пойдёт не так. Сначала он проверяет версию PHP на сервере и останавливается, если она слишком старая для приложения. Затем сохраняет статический сайт полной копией рядом с новым. После этого направляет сайт на приложение Laravel и ждёт, пока сайт трижды подряд ответит правильно: страница 404, вход в админку и одна публичная страница, байты которой должны совпасть со статической копией.

Затем он запускает живую проверку, которая спрашивает настоящий сайт через интернет, что увидит посетитель:

  • каждая страница из карты сайта, побайтно сравнённая со статической копией;
  • сама карта сайта, с теми же адресами и корректными датами;
  • robots.txt, страница 404 и перенаправления на адрес с косой чертой в конце, на https и на домен без www;
  • стили, скрипты и поисковый индекс;
  • ни одной cookie на публичных страницах;
  • открытая страница входа в админку и закрытая для гостей админка;
  • проба формы заявки, которую форма обязана отклонить до того, как что-то запишется или отправится.

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

Каждый шаг переключения умел отменить себя сам, в этом и был смысл.

Сюрприз

Ловушка на одну минуту

Первая попытка показала то, о чём нам не говорило ни одно руководство. Сразу после переключения примерно минуту часть запросов падала с ошибкой «Failed opening required»: PHP всё ещё искал файлы приложения по старому пути.

Причина в кэше realpath в PHP. Чтобы не делать лишнюю работу, PHP запоминает, куда ведёт каждый путь, и по умолчанию хранит этот ответ 120 секунд. Переключение изменило, куда указывает папка сайта, а PHP на сервере ещё до двух минут держал старый ответ. Ничего не сломалось, он просто отвечал по памяти.

Закрыли это двумя изменениями. Входной файл приложения теперь подключает свои файлы по абсолютным путям и больше не зависит от того, куда ведёт папка сайта. А скрипт переключения ждёт трёх правильных ответов подряд, прежде чем запускать живую проверку, и не верит первому. С обоими изменениями проверки прошли сразу.

Итог

Что изменилось и что нет

Живая проверка закончилась результатом 47 из 47, все страницы совпали. Для читателя сайт выглядит и работает так же, как накануне. Googlebot пришёл через несколько минут после переключения, прочитал несколько статей из раздела Insights и получил те же страницы со статусом 200, что и от статической сборки.

Изменилось всё, что стоит за страницей. Статьи, авторы и страницы правятся в админке, а не в файлах Python. Заявки записываются до отправки любого письма и живут в своём разделе, с корзиной для тестовых. Панель показывает поиск, аналитику и визиты краулеров в одном месте. Карта сайта говорит правду о датах. Статическая сборка ушла на покой, а её копия лежит рядом с живым сайтом.

Кто

Как разделили работу

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

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

Выводы

Что это значит для вашей системы

Большинство систем, которые мы видим, не новые сайты. Это старые приложения на устаревающем фреймворке, сайты, оставленные прежним подрядчиком, или продукт, который никто не решается менять, потому что никто не знает, что сломается. Такой переезд и есть наш подход к ним: определить, что должно остаться прежним, до того как что-то трогать, измерять это скриптом, а не на глаз, держать путь назад, который работает одной командой, и менять по одной вещи за раз.

Так же работают у PERETZ приём проекта от другого подрядчика и разработка на Laravel, и так же мы провели свой сайт через шесть версий Laravel за три дня. Технический долг можно спокойно передать, когда те, кто его забирает, могут показать, страница за страницей, что ничего не сломалось. The Structure применяет ту же дисциплину к компании на проверке, а его подход описан слой за слоем.

Взгляд проверяющего на этот переезд, то есть что каждая находка значит, когда миграция прошла в компании, которую покупают или продают, опубликован на сайте The Structure (на английском): What a migration changes when nothing is supposed to change.

FAQ

Вопросы об этом переезде

Почему не пересобрать сайт на Laravel и не смириться с мелкими отличиями?

Потому что новый сайт в первые недели ещё изучают поисковые системы, а любое лишнее изменение во время переезда может стать для Googlebot проблемой. Мелкие отличия в разметке складываются на каждой странице, и быстро понять, какие из них краулеру важны, нельзя.

Как побайтно сравнить сайт на Laravel со статическим?

Команда artisan собирает запрос для каждого публичного адреса, пропускает его через HTTP-ядро приложения и сравнивает ответ со статическим файлом. Она печатает отличающиеся строки, так что каждое отличие можно прочитать и исправить.

Какое отличие между Python и PHP оказалось самым коварным?

Округление. Python округляет половину к чётному числу, а PHP от нуля, поэтому время чтения 10,5 минуты в одном показывается как 10, а в другом как 11. Небольшой вспомогательный класс повторяет поведение Python там, где это важно.

Что такое кэш realpath в PHP и почему он был важен?

PHP запоминает, куда ведут пути к файлам, по умолчанию на 120 секунд. Когда папку сайта направили на новое приложение, PHP ещё до двух минут держал старый ответ. Помогли абсолютные пути во входном файле и ожидание трёх правильных ответов подряд.

Как проверить форму заявки во время переключения, ничего не отправив?

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

Можете сделать то же самое для нашей системы?

Да. Перенос старого приложения или сайта, оставленного другой командой, для PERETZ обычная работа. Мы начинаем с того, что записываем, что не должно измениться, а потом измеряем это до и после каждого шага.

Источники

Боитесь трогать систему, потому что никто не знает, что сломается? Давайте посмотрим, что должно остаться прежним.

Записаться на стратегическую сессию

Приём проекта от другого подрядчика