Як ми перевели The Structure на Laravel, не змінивши жодного байта
The Structure перевіряє технології компаній перед угодою. Коли його власний сайт у перші дні після запуску довелося перевести зі статичної збірки на Laravel, ми провели переїзд за тим самим стандартом, якого бренд вимагає від інших: кожне твердження підтверджено доказом, а для читачів і пошукових систем не змінилося нічого.
The Structure це бренд технічного due diligence для угод із софтверними компаніями, який ми виділили з 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, маршрутів і шаблонів. Більша частина переносу була звичайною роботою. Цікавими виявилися дрібні місця, де дві мови по-різному відповідають на, здавалося б, одне й те саме питання і де один неправильний символ проявився б на кожній сторінці.
| Що | Python | PHP за замовчуванням |
|---|---|---|
| Округлення половини | round(2.5) дає 2: половина йде до найближчого парного | round(2.5) дає 3: половина йде від нуля |
| Поділ за пробілами | str.split() вважає пробілами нерозривні пробіли і старі керівні роздільники | звичайний \s у регулярному виразі PHP частину з них пропускає |
| Запис JSON | json.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 звичайна робота. Ми починаємо з того, що записуємо, що не має змінитися, а потім вимірюємо це до і після кожного кроку.
Джерела
- Google Search Central: Move a site with no URL changes
- Документація Python: round()
- Документація Python: json.dumps
- Посібник PHP: round
- Посібник PHP: realpath_cache_ttl
Боїтеся чіпати систему, бо ніхто не знає, що зламається? Подивімося, що має лишитися незмінним.
Раз на тиждень одне сповіщення з новими статтями PERETZ. Нічого зайвого.