Source: https://alexjardinedigital.com/work/nextjs-migration
Author: Alex Jardine

Own site · Platform migration

# Next.js migration

I moved this portfolio from static HTML to Next.js and checked that nothing visible changed. Of 212 before-and-after screenshot pairs, 208 were pixel-identical, and the four small differences are measured in the report. Search engines now get clean URLs, real 404s and structured data.

Ownership **My own site**

Completed **October 2026**

Stack **Next.js 16 · React 19 · TypeScript**

Scope **16 routes · 10 case studies**

 [Read case study](<https://alexjardinedigital.com/work/nextjs-migration#csx-title>)

## The short version

What it is

A design-led portfolio, finished and approved as frozen static HTML, that needed a real platform: server-rendered pages, clean URLs and the metadata search engines read, with no redesign along the way.

What I built

A Next.js 16 App Router site that reuses the original CSS, fonts, images and particle code byte for byte, adds clean routes, per-page metadata, structured data, a sitemap, robots.txt, RSS and real 404s, and shipped with a written parity record.

Why it stands out

Evidence that the move was safe. Every page was captured before and after at four widths and compared pixel by pixel, and the few differences are listed in the report.

**212** Before-and-after screenshot pairs Five page templates at four widths

**208** Pixel-identical The other four: no colour channel off by more than 5 of 255

**176** Route and width combinations Zero horizontal overflow

**54** Reference files fingerprinted The build fails if one changes

From the migration report and comparison data, 3 October 2026. The moving particle field, cursor and live clocks were masked in both builds; nothing else was.

The brief

## A new platform  nobody should notice.

The design was finished and approved as a set of static HTML pages: hand-tuned type, a WebGL particle field, scroll choreography. It looked right, but it was a prototype. All ten case studies shared one page,  **project.html?p=lambert**  and its siblings, which arrived as an empty shell for JavaScript to fill in. An unknown project was just a message drawn by a script.

The job was to put it on a production platform without redesigning it. Any visible change had to be deliberate and written down. Everything else had to land exactly where it was, at every width, in every case study.

Same page

## Same page.  What Google receives.

A visitor sees the Lambert case study exactly as before. A crawler’s first request for it is where everything changed.

First response project.html?p=lambert → /work/lambert  **+11**   **−2**

```
− <title>Case study · Alex Jardine</title>
+ <title>Lambert Lighting · Alex Jardine</title>
+ <meta name="description" content="A specification-ready lighting catalogue: 13 static …">
+ <link rel="canonical" href="https://alexjardinedigital.com/work/lambert">
+ <meta property="og:title" content="Lambert Lighting · Alex Jardine">
+ <script type="application/ld+json">{"@type":"CreativeWork", …}</script>
+ <script type="application/ld+json">{"@type":"BreadcrumbList", …}</script>
  </head>
  <body>
− <main id="main"></main>
+ <main id="main">
+   <h1>Lambert Lighting</h1>
+   <p class="lede">Lambert makes architectural lighting in Rhode Island. I rebuilt …</p>
+   … the rest of the case study, over 1,200 words
+ </main>
```

Real responses: the frozen prototype file and the live page, trimmed to the lines a crawler reads first. The prototype’s ` <main> ` held no words until JavaScript ran.

![Lambert Lighting case study hero at 1440 pixels wide, identical in the static HTML reference and the Next.js build.](<https://alexjardinedigital.com/media/cases/migration/lambert-next-1440.webp>)

The Lambert case study at 1440 px, captured at the migration on 3 October 2026 (its copy has been rewritten since). Below the header, the captures from the static prototype and the Next.js build are the same file, so there is nothing to compare by eye. The original stylesheet, fonts and images were reused unchanged.

How it was done

## How the move  was checked.

Before any code moved, the approved reference was fingerprinted. Fifty-four source files are hashed, and the production build still refuses to run if one of them changes. If the approved design is edited by accident, the build stops.

The port reused files rather than rewriting them. Search infrastructure was added underneath, and then every page was captured in pairs, from the same browser at the same viewport, and compared. The written record shipped with the code.

The migration sequence

1. **Freeze the reference** Hash every approved source file
2. **Port without restyling** Reuse CSS, fonts, images and scripts as they are
3. **Add what search needs** Clean URLs, metadata, structured data, sitemap, real 404s
4. **Capture in pairs** Same browser and viewport, four widths, moving layers masked
5. **Measure every difference** Per-pair pixel deltas saved beside the screenshots
6. **Ship with the record** A report listing each difference and each untested area

Search

## What crawlers  receive now.

Visitors see the same site. The HTML a crawler receives on the first request is completely different.

What a search engine receives, now and before

| Signal | Next.js build | Static prototype |
| --- | --- | --- |
| Case study address | A clean path per case study, such as /work/lambert | Was project.html?p=lambert |
| First response | The whole page, rendered on the server and readable without JavaScript | Was an empty shell |
| Unknown project | A real 404 status with a readable page | Was a script message |
| Titles and descriptions | Per route in the first HTML, with canonical and social cards | Was set by script, no description |
| Structured data | WebSite, Person, CreativeWork, and Article when published | Was none |
| Sitemap and robots.txt | Generated from the same project data as the pages | Was none |
| Feed | RSS, published articles only | Was none |
| Unpublished sample | noindex, and kept out of the sitemap and feed | Was an ordinary page |

The prototype was a local design reference, not the public site, so launch needed no redirects. On a live site this table becomes the redirect map: every old address, its new home and a permanent redirect between them.

Calls I made

## Four decisions  during the move.

### Reuse the original files

Chose

The original image files and stylesheet, served as they were.

Instead of

Framework image optimisation and a rewritten stylesheet at launch.

Trade-off

Some optimisation waited for a later, separately checked change, in exchange for zero crop or rendering drift on day one.

### Report every difference

Chose

Report the four imperfect pairs with their exact deltas.

Instead of

Raising the tolerance until every pair passed.

Trade-off

The headline is 208 of 212 instead of 212, and anyone can check the record.

### Mask only the moving layers

Chose

Mask the particle field, cursor and clocks, and nothing else.

Instead of

Masking any region that happened to differ.

Trade-off

Animation cannot be pixel-compared, so it was checked separately by behaviour and frame timing.

### Leave an unreproducible issue open

Chose

Record a first-load text size variation in the original that twelve fresh test runs could not reproduce.

Instead of

Shipping a speculative runtime fix.

Trade-off

One open review item in the report instead of new code with unknown side effects.

In-house · Craft CMS to Webflow

## Copper CRM,  200+ pages.

I led SEO for Copper CRM’s move of its 200+ page marketing site from Craft CMS to Webflow, working with the in-house team. My part was the redirect and URL map, the content migration and the launch SEO QA.

That was a live site with existing search history, where a migration carries real risk: every old address needs a new home, and every redirect has to work on launch day. Copper’s working files stay with Copper, so the evidence on this page comes from a site I own.

Result

## Where it  stands now.

The Next.js build went live in October 2026 and is the site you are reading. Everything added since has been built on it.

Shipped

- Server-rendered pages at clean /work/ addresses
- Per-page titles, canonicals and social cards
- Structured data, sitemap, robots.txt and RSS
- Real 404s for unknown projects and articles
- Build checks that fail on reference drift

Not claimed

Safari, physical phones and screen readers were outside this test pass. There are no traffic or ranking figures, because the prototype had no search history to compare against. Moving particles were checked by behaviour, not pixels.

For a client migration

The same method carries over: fingerprint the current site, map every URL, port without redesigning, capture pairs, and launch with a written record of what changed and what was not tested.

## Related work

[Same discipline, larger system **Jardine Studio** Jardine Studio checks for search regressions on every pull request.](<https://alexjardinedigital.com/work/jardine>) [In the comparison above **Lambert Lighting** The client site used as the before-and-after example.](<https://alexjardinedigital.com/work/lambert>)

[All projects ↗](<https://alexjardinedigital.com/work>)
