Type at least 3 letters to search

·12 min read

Moving The Structure to Laravel Without Changing a Single Byte

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 reviews the technology of companies before a deal. When its own site had to move from a static build to Laravel in its first days online, we held the move to the standard it asks of others: every claim backed by evidence, nothing changed for readers or search engines.

The Structure is the technical due diligence brand we spun out of PERETZ for software M&A. It reads the code, the infrastructure, the data and the ownership of a company for the people who buy and sell it, and it asks one thing of every system it reviews: show the evidence. This article is about the first system it had to examine, which was its own site.

The site launched as a static build: a Python generator wrote fifty HTML files, a sitemap and a search index, and the server handed them out. That was the fastest way to go live. A few days later it had to move to Laravel, so the team could run it the way we run peretz.agency, with an admin for articles, authors, pages and inquiries, and one dashboard for search and analytics.

The move had a single rule. Nobody should notice it, neither the people reading the site nor the search engines that had only just started to visit. So the goal became a site that renders the same bytes as before, page by page, with a way back that takes one command. It is written by Peretz Group, which on this project means the founder, who set the rules and made the calls, and Claude, the AI that did the technical work under review.

Why

A move nobody should notice

A new site is at its most fragile in its first weeks online. Search engines are deciding what it is, which pages to keep and how often to come back. Google's guide to moving a site without changing its addresses treats a change of the serving infrastructure as a move of its own, and says that as long as Googlebot meets no serious problems or slowdowns, it will crawl the site as fast as it needs to. A new framework under the same addresses is that kind of move, and it is easy to add a problem without meaning to: a reordered attribute, a different date format, a new cookie, a page that answers a little slower.

There was a second reason, closer to the brand. The Structure tells founders that a buyer will test every claim in their sale document, and that a claim without evidence becomes a question in the middle of exclusivity. It would be odd to tell clients that and then move our own site on a feeling that it looked fine.

We moved our own site the way we ask sellers to prove claims.

Standard

What the same meant

Before any code was written, we defined "the same" in a way a script can check. Every public address renders through the new application and is compared, byte for byte, with the file the static build wrote for it. That covers the fifty HTML files, the sitemap with all 33 addresses, robots.txt, the 404 page and the search index the site's own search reads in the browser.

The comparison runs as an artisan command. It builds a request for every address, passes it through the application's HTTP kernel, takes the response and compares it with the static file on disk. A page that differs prints the first lines that differ, the old one above the new one, so a difference is a line to read, not a guess. The command was run after every change, and the move waited until it reported that all pages were the same.

One difference was allowed on purpose. The static sitemap gave every page the date of the last build, which is the date the generator ran rather than the date anything changed. The Laravel version gives each page the date it was really last edited: the article date or its latest update, the admin edit date for pages and authors, and a fixed base date for pages nobody has touched since launch. It is the one place where the new site says something more accurate than the old one, so the check compares the sitemap addresses and leaves the dates out.

Details

Where Python and PHP disagree

The static generator was about 2,700 lines of Python in twelve files. The new application is about 3,500 lines of PHP, routes and templates. Most of the port was plain work. The interesting part was the small places where the two languages give different answers to what looks like the same question, and where one wrong character would show up on every page.

WhatPythonPHP by default
Rounding a halfround(2.5) gives 2: halves go to the even neighbourround(2.5) gives 3: halves go away from zero
Splitting on spacesstr.split() treats non-breaking spaces and old control separators as spacesa plain \s in a PHP regular expression misses some of them
Writing JSONjson.dumps puts a space after every comma and colonjson_encode writes no spaces and escapes every slash
Escaping quoteshtml.escape turns an apostrophe into 'htmlspecialchars writes '
Dates"%b %-d, %Y" gives Nov 9, 2026the same date needs the format 'M j, Y'

Reading time on each article is counted from words and rounded, so the rounding rule decides whether a card says ten minutes or eleven. Structured data is written as JSON inside the page, so the spaces after commas are part of the bytes. Titles with apostrophes appear in meta tags, so the escape sequence matters on every article that has one.

The answer was a small helper class of about a hundred lines that does what the Python functions did: the same rounding, the same idea of a space, the same JSON layout, the same escaping and the same dates. Every template uses it where the generator used Python, and the comparison proved it page by page.

Content

The same pages, now with an admin

Once the pages matched, the content moved from Python files into a database, and the site got an admin in the same look as the PERETZ admin, in English only, with The Structure's own colours. Articles are written in a rich-text editor with their FAQ and up to two authors, and on saving the admin warns about our house rules, such as long dashes, a text that is too short or an FAQ with fewer than three questions. Authors, pages, inquiries, users and settings each have their own section.

The admin home is a dashboard: inquiries, the content in numbers, which search and AI crawlers came to the site and, as each service grants its access, search and analytics data brought in by a nightly job. It reuses the approach from the indexing audit we built for peretz.agency.

The public pages kept the behaviour of the static site. They are the same for every visitor and set no cookies, as before; sessions exist only inside the admin. When an editor saves a page, the site rewrites its search index, so the visitor's search sees the change at once.

Switch

One command, with the way back built in

The switch itself was one script, and most of it is about what happens if something goes wrong. It checks the server's PHP version first and stops if it is too old for the application. It keeps the static site as a full copy beside the new one. Then it points the site at the Laravel application and waits until the site gives three correct answers in a row: the 404 page, the admin login and one public page whose bytes must match the static copy.

Then it runs the live check, which asks the real site over the internet what a visitor would see:

  • every page in the sitemap, compared byte for byte with the static copy;
  • the sitemap itself, with the same addresses and valid dates;
  • robots.txt, the 404 page and the redirects to the address with the trailing slash, to https and to the domain without www;
  • the stylesheets, the scripts and the search index;
  • no cookies on any public page;
  • the admin login page open, and the admin closed to guests;
  • a probe of the inquiry form that must be rejected before anything is logged or sent.

That is 47 checks. If any of them fails, the script switches the site back to the static copy on its own and says which check failed. Going back by hand is one command as well. The probe of the form deserves a word: it posts a request the form must refuse, so the check proves the form is alive without a single test inquiry reaching the log or anyone's mail.

Every step of the switch could undo itself, and that was the point.

Surprise

The one-minute trap

The first attempt showed something no guide had mentioned to us. Right after the switch, for about a minute, some requests failed with "Failed opening required": PHP was still looking for the application's files along the old path.

The cause is PHP's realpath cache. To save work, PHP remembers how each path resolves, and by default it keeps that answer for 120 seconds. The switch changed where the site's folder points, and for up to two minutes the server's PHP kept the old answer. Nothing was broken, it was just answering from memory.

Two changes closed it. The entry file of the application now loads its files by absolute paths, so it no longer depends on how the site's folder resolves. And the switch script waits for three good answers in a row before it runs the live check, rather than trusting the first one. With both in place, the checks passed at once.

Result

What changed and what did not

The live check ended with 47 of 47 and all pages the same. For a reader the site looks and works as it did the day before. Googlebot visited within minutes of the switch, read several insights articles and got the same pages with status 200, as it had from the static build.

What changed is everything behind the page. Articles, authors and pages are edited in an admin instead of in Python files. Inquiries are recorded before any email goes out and have their own section, with a bin for tests. The dashboard shows search, analytics and crawler visits in one place. The sitemap tells the truth about dates. The static build was retired, with its copy kept beside the live site.

Who

How the work was shared

The founder set the rules: nothing visible changes, the way back is ready before the way forward, no test inquiry ever reaches anyone, the admin looks like the PERETZ admin. He made the calls that needed judgement, such as which bots the site should let in and when to switch.

Claude did the technical work: the port, the helper class, the parity command, the live check and the switch script, and the diagnosis of the one-minute trap. AI is good at exactly this kind of patience: comparing fifty pages byte by byte after every change, and reading a difference line by line instead of calling it close enough. It was also told what it could not decide alone, and it stopped and asked when a step would touch something outside the plan.

Lessons

What this means for your system

Most of the systems we see are not new sites. They are old applications on an aging framework, sites left by a previous contractor, or a product that nobody dares to change because nobody knows what will break. A move like this one is how we approach them: define what must stay the same before touching anything, measure it with a script rather than an eye, keep a way back that works in one command, and change one thing at a time.

That is also how project takeover and Laravel development work at PERETZ, and how we moved our own site through six Laravel versions in three days. Technical debt is safe to hand over when the people who take it can show, page by page, that nothing broke. The Structure applies the same discipline to a company under review, and its approach describes it layer by layer.

The reviewer's side of this move, what each finding means when the migration belongs to a company being bought or sold, is on the site of The Structure: What a migration changes when nothing is supposed to change.

FAQ

Questions about this move

Why not rebuild the site in Laravel and accept small differences?

Because search engines are still learning a new site in its first weeks, and any extra change during a move can become a problem for Googlebot. Small differences in markup add up across every page, and there is no quick way to know which of them a crawler minds.

How do you compare a Laravel site with a static one byte for byte?

An artisan command builds a request for every public address, passes it through the application's HTTP kernel and compares the response with the static file. It prints the lines that differ, so each difference can be read and fixed.

What was the hardest difference between Python and PHP?

Rounding. Python rounds halves to the even number and PHP rounds them away from zero, so a reading time of 10.5 minutes shows as 10 in one and 11 in the other. A small helper class copies the Python behaviour where it matters.

What is the PHP realpath cache and why did it matter?

PHP remembers how file paths resolve, for 120 seconds by default. When the site's folder was pointed at the new application, PHP kept the old answer for up to two minutes. Absolute paths in the entry file and a wait for three good answers in a row solved it.

How do you test a contact form during a switch without sending a message?

The check posts a request the form must refuse. If the form answers with its error page before logging or mailing anything, it is alive, and no test inquiry reaches anyone.

Can you do the same for our system?

Yes. Moving an old application or a site left by another team is regular work for PERETZ. We start by writing down what must not change, then measure it before and after every step.

Sources

Afraid to touch a system because nobody knows what will break? Let's look at what has to stay the same.

Book a Strategic Session

Project Takeover