Skip to content
Back to blog
  • Engineering

Why I Built a UK Marketplace on PHP, Not Next.js

8 min read

Next.js is my default. I reach for it on most new builds, and I recommend it to most founders who ask. So it is worth explaining why CarVendors — a live UK used-car marketplace I lead the development on — runs on PHP and MySQL instead, what that decision bought, what it cost, and where the interesting engineering actually ended up.

This is the engineering companion to the CarVendors case study. That page covers what the system does for the business. This one is about the trade-offs underneath.

The decision was mostly made before I arrived

The honest version first: CarVendors already existed. There was a working marketplace on a purchased UI template, a live admin panel, real vendors and real stock. My job was not to choose a stack from nothing — it was to rebuild the parts that were failing while the site stayed up.

That reframes the question. "PHP or Next.js" is a fun argument on a blank page. On a system with live listings and paying dealers, the real question is: what can I change without downtime, and what is the smallest change that fixes the actual problem?

A rewrite would have been the most expensive possible answer to "our stock does not update automatically."

What I would have gained from a rewrite, honestly

Not nothing. A typed boundary between the UI and the data layer catches a whole category of bug at compile time, and on a marketplace where most content is written by vendors and importers rather than by you, that matters more than usual. React's component model beats string-concatenated HTML for anything stateful. The deployment story is better.

But look at what CarVendors actually needed:

  • Stock ingestion from dealer sites with no API
  • Search that composes many optional filters into one fast query
  • Listing pages that render fast and stay crawlable

Only the third has much to do with the frontend framework. The first is a scheduled process talking to HTTP and a database — a domain where PHP is entirely unremarkable and completely adequate. The second is SQL. Rewriting the view layer would not have made either better.

The constraint that settled it: the whole thing runs on shared cPanel hosting. Not a preference — the client's existing arrangement. PHP and cron are native there. A Node runtime, a build step and a process manager are not.

Where the engineering actually went

Change detection, and why ordering matters more than hashing

The ingestion pipeline reduces each vehicle to a twelve-field content fingerprint — title, price, mileage, description, model, year, fuel, transmission, colour, engine size, body style, image count — with text fields normalised first, so cosmetic churn on the source site does not register but a real price drop does.

The hash itself is boring. What matters is where it sits. The fingerprint is checked first, in a single indexed lookup, before any attribute write, any enrichment call, any image work. A match short-circuits the entire per-vehicle pipeline.

Put the check after the expensive work and you have an optimisation that saves one write. Put it first and an unchanged car costs one indexed read. That is the difference between refreshing stock once a day and refreshing it several times a day, and it is a design decision, not a framework feature.

Swapping a database view under a live site

Search read from a view with eleven joins, a GROUP BY and GROUP_CONCAT. The replacement was flatter: six joins, no aggregation.

The problem is deployment. Change the code and the view together and you need a synchronised code-and-database release — on shared hosting, with no migration tooling, on a live marketplace. Get the order wrong and search breaks for real buyers.

So the query layer detects which view exists at runtime, with a cheap probe, and adapts its column list accordingly. Deploy the code first: it keeps using the old view because the new one is not there yet. Deploy the view whenever: the next request picks it up. Roll back by dropping the view.

The cost is a branch in the code and a column alias to reconcile a renamed field. The benefit is that a schema migration stopped being an event. I would take that trade on any stack.

N+1 in a place that is easy to miss

Each listing card needed its photos, its saved-state flag and a couple of counts. Rendered one at a time, a 24-card results page put well over a hundred queries behind the page carrying all the commercial traffic — the classic N+1, hiding inside a render function rather than inside a loop where you would look for it.

The fix is not clever: collect the IDs for the whole page first, fetch everything in a small number of batched queries, hand the card renderer a cache. A results page now costs five queries regardless of card count, four for signed-out visitors. Card-view analytics went the same way — one batched insert at the end of the request instead of one per card.

Worth naming the related change: all the shared database objects were made to reuse a single connection rather than opening their own. Connection setup was a fixed cost paid several times on every page load, which is the kind of overhead that never shows up in a profiler you did not run.

A slider, hand-rolled

The listing card carries an image gallery. The site already loaded a slider library, and I did not use it — the card needed touch-swipe, lazy loading and navigation arrows that stay visible on touch devices, and it needed to render inside a loop without initialising a plugin instance per card.

Event delegation on a container handles any number of cards with one listener set. The supporting CSS and JS inject once per page behind a static flag, not once per card. This is the sort of thing a component framework gives you for free, and writing it by hand is the clearest single cost of staying on PHP. It is maybe two hundred lines. It is also the only place in the rebuild where I genuinely missed React.

What PHP actually cost

Three things, and they are real.

No type safety across the data boundary. Most content comes from vendors and importers, so bad data is the normal case, not the exception. Validation is written by hand and enforced by discipline. A typed schema would catch a class of bug that currently reaches production.

Presentation and logic are tangled. Large files mix PHP, HTML, CSS and JavaScript. It works and it is fast, but it is hostile to a newcomer, and "where does this get rendered" is a genuine question rather than a lookup.

Testing is harder than it should be. There are no automated tests on the ingestion pipeline or the search query builder — the two places where a silent bug does the most damage. That is a gap I would close first, and the architecture makes it more work than it would be elsewhere.

None of those were worth a rewrite. All of them are worth naming.

When I would still choose Next.js

For a new marketplace with no legacy and no hosting constraint, I would. Server-rendered listing pages stay indexable, the typed boundary earns its keep exactly where marketplace data is messiest, and one stack covers frontend, API routes and deployment.

The decision inverts on three signals: an existing system with live users, hosting you do not control, and a problem that lives in the data layer rather than the interface. CarVendors was all three.

The lesson I would actually pass on is that the framework question is usually less interesting than it feels. The things that made CarVendors work — checking the fingerprint before the expensive work, letting the query layer discover its own schema, batching the page's queries instead of the card's — are all decisions you would make identically in TypeScript. The stack decides how pleasant they are to write. It does not decide whether you thought of them.

FAQ

Would you migrate CarVendors to Next.js now?

Not as a project in its own right. If the frontend needed a significant rebuild for other reasons, then the calculation changes and I would probably do it then. Migrating a working system to a stack I prefer is not a business case.

Does PHP hurt SEO compared to Next.js?

No. Server-rendered HTML is server-rendered HTML. Filtered search URLs on CarVendors are real, crawlable pages, which is the thing that actually matters for a marketplace. Page speed is a function of query count, payload and caching — all of which are stack-independent, and all of which I had to work on anyway.

What is the single biggest technical risk in the current design?

The import is linear, so there is a practical ceiling on how much stock one run can handle on this hosting. Past that it needs chunked runs across cron windows or a bigger box. It is designed and not built, which is the honest status.

Next step

If you are weighing a rebuild against fixing what you have, the CarVendors case study covers what the rebuild delivered for the business. For the commercial side of this kind of work — dealer stock feeds, vehicle search, automotive marketplaces — see automotive marketplace and dealer stock feed development.

Got a project like this?

Tell me what you're building — I take on a small number of build-and-ship projects each quarter. Reply within 24 hours.