I spent today doing the sort of “one-inch-deep, one-mile-wide” work that quietly makes a site sturdier. The goal was to modernise the stack, fix a visual jolt on the Work page, and leave the UI looking exactly as it did before.
Codex and GPT-5 did most of the fetching and carrying: fast at the automation, steady on the guard-rails, and surprisingly good at spotting the boring jobs I’d normally keep putting off.
The upgrades, and why they matter
- React 19 and Next 15.5: unlocks the newer server action ergonomics (
useActionState), better dev ergonomics, and compatibility with modern libraries. - Tailwind CSS 4: smaller, faster CSS with a more predictable palette. It needs some PostCSS and config changes.
- TypeScript and tooling:
moduleResolution: 'bundler'to keep type checking happy with ESM packages, such as Tailwind v4’s.d.mtstypes. - Node 22 on Vercel: the same runtime locally, in CI, and at build time, so no more engine mismatch surprises.
- Vercel Speed Insights: bumped to ^1.2.0 and loading cleanly.
The “oh, that’s why” moments
-
The CLS on the Work page was self-inflicted. A “non-blocking CSS” trick that flipped
media=printwas causing FOUC and layout shift. We removed it and let Next do its thing, and the page settled down immediately. -
Tailwind v4 is excellent, and opinionated. Two gotchas:
@applycan be prickly with palette tokens in v4. Using Tailwind’s CSS variables in CSS, likevar(--color-neutral-300), cleared the handful of “unknown utility” errors while keeping the look.- Gray versus neutral. We briefly migrated to
gray-*across the UI, then found the site looked better and more consistent onneutral-*. We reverted carefully so the original tone survived the upgrade.
-
Builds should be boring. We made them that way:
- Enforced Node 22 in
vercel.jsonand.nvmrc. - Swapped PostCSS to
@tailwindcss/postcssfor Tailwind v4. - Fixed TypeScript’s type resolution for Tailwind (
moduleResolution: 'bundler'). - Kept the unit tests green, adjusting a couple of assertions for React 19 and the palette changes.
- Enforced Node 22 in
The refactors that paid off
- Contact form: switched to React 19’s
useActionStateand server actions. The “invalid action prop” warning is gone and the intent is clearer. - Theming: standardised on the site’s neutral palette and dark mode variables, so borders, cards, and pills read clearly in both themes.
What Codex and GPT-5 did well
Think of it as an extremely diligent junior engineer. It sweeps the repo quickly for semantic changes, like every neutral-* against gray-* class. It handles rote edits and lockfile bumps without bikeshedding. It surfaces failing tests and noisy build logs you’d otherwise scroll past. And it doesn’t forget to push, ignore odd files, or align the CI settings.
If you’re curious about how I use AI on real projects, I wrote about the workflow in my post on AI coding tools, and why I keep human attention for the creative parts.
What I’d improve next
- A small design pass on dark mode link contrast, so hover states feel more deliberate.
- A Lighthouse and Speed Insights sweep to put numbers on the CLS and style fixes.
- A short visual regression checklist for Tailwind v4 migrations.
Takeaways
- Upgrade in thin slices, validate, and keep the look. Framework changes shouldn’t force style regressions.
- Let the tools handle the repeatable changes while you keep taste and intent.
- Boring builds are a feature. Pin Node, fix PostCSS, keep types happy, and the pipeline stops being something you think about.
Small, correct, and calm beats flashy and fragile. Today was the first kind, which is a good day.
Update: Tailwind 4 follow-up (2025-09-27)
After publishing, I finished the Tailwind v4 migration with two fixes that might save you some time:
-
MDX typography disappeared. Headings and spacing looked flat on MDX pages. Under Tailwind v4 I had to declare the Typography plugin in the CSS entry rather than relying on
tailwind.configalone.Add to
app/tw.css:@import "tailwindcss"; @plugin "@tailwindcss/typography";That regenerated the
.prosestyles and restored sensible heading sizes and spacing. I kept a few bespoke.proseoverrides inapp/global.cssfor anchors, code blocks, and top margins. -
Work page logos bottom-aligning with no description. Some entries without a description, like WHSmith, West Hill, and ASFC, showed the logo hugging the bottom. The cause was a brittle grid template value that compiled with a comma and behaved inconsistently across browsers. Rather than fight it, I simplified the layout:
- Header row: a flex container with text on the left and the logo on the right (
items-startandjustify-between). - Description, when present: a second block below the header.
The logo now stays top right whether or not there’s a blurb, and it reads more cleanly in the DOM.
- Header row: a flex container with text on the left and the logo on the right (
Net effect: the Tailwind v4 performance and DX benefits, with the original look preserved on MDX and the Work page.