Back to Home

Projects

14 Jul 2026

·

6 min read

·

Next.js · TinaCMS · Migration

Leaving WordPress Without Losing the Editors

How IT Step School moved from a WordPress theme-and-plugin stack to Next.js with a git-based CMS — without a content freeze, without losing URLs, and without taking the editing experience away from staff.

Every WordPress migration I have seen fail failed for the same reason: the engineers optimised for the stack they wanted and forgot who actually uses the site every day. At IT Step School that was the marketing and admissions staff. They did not care about React. They cared that adding a news post took two minutes and did not require asking anyone.

So the brief I set for the team was two-sided. Performance, security, and maintainability had to improve — the legacy theme plus plugin stack made every page heavy and every update risky. But the editors had to keep a familiar editing experience, or the migration would be reverted in practice even if it shipped in production.

Why a git-based CMS

We chose Next.js for the front end and a git-based headless CMS for content. Content lives as files in the same repository as the code; editors work in a visual editing UI that commits on their behalf. That gave us three things WordPress never could:

  • Versioned content. Every change is a commit. Rolling back a broken page is a revert, not a database restore.

  • One deployment path. Content and code go through the same CI/CD pipeline, so there is no separate ‘content server’ to secure and patch.

  • Static output. Pages are generated at build time and served by Nginx as files. There is no PHP process to exploit and nothing to slow down under load.

The trade-off is that a commit-based workflow feels slower than ‘click publish’ if the build takes minutes. We kept builds short and made preview part of the editing UI, so editors see the result before it ships.

INTERACTIVE FIGURE
Before and after: where a page comes from

Migrating without a content freeze

The school kept publishing during the migration — enrollment season does not pause for engineering. We handled that with three rules.

1. Map fields, not pages

We modelled the new content types first (program, news article, page, banner), then wrote a mapping from each WordPress post type and its custom fields into those types. The migration script ran repeatedly against a fresh export, so anything published on the old site after the first run simply came along on the next.

for (const post of wpExport.posts.filter(p => p.type === "news")) {
  writeMarkdown(`content/news/${slugOf(post)}.md`, {
    title: clean(post.title),
    date: post.date,
    cover: await mirrorMedia(post.featured_image),
    body: htmlToMarkdown(post.content),
  })
}
for (const post of wpExport.posts.filter(p => p.type === "news")) {
  writeMarkdown(`content/news/${slugOf(post)}.md`, {
    title: clean(post.title),
    date: post.date,
    cover: await mirrorMedia(post.featured_image),
    body: htmlToMarkdown(post.content),
  })
}
for (const post of wpExport.posts.filter(p => p.type === "news")) {
  writeMarkdown(`content/news/${slugOf(post)}.md`, {
    title: clean(post.title),
    date: post.date,
    cover: await mirrorMedia(post.featured_image),
    body: htmlToMarkdown(post.content),
  })
}

2. Preserve every URL

Search rankings were the school’s lead source. We generated a redirect map from the old permalinks to the new routes and verified it against the crawl of the old site before cut-over. Nothing indexed was allowed to 404.

3. Verify, then cut over

The new site ran in parallel on a staging host with the full migrated content. We diffed page inventories and spot-checked the pages editors cared about most, then switched DNS. The old site stayed reachable for a week as a fallback.

INTERACTIVE FIGURE
The idempotent migration loop

Performance as a property of the architecture

We did not ‘optimise’ the site at the end. Static generation, responsive image optimisation, and a per-page JavaScript budget made speed a default. The campus gallery — the heaviest page — went from full-size uploads to responsive variants served by Next.js image optimisation without editors changing anything about how they upload.

What I would do again

  • Decide the editor experience before the stack. The stack serves it, not the other way round.

  • Make the migration script idempotent from day one. It removes the freeze problem entirely.

  • Treat the redirect map as a deliverable with its own review, not a cleanup task.

The result is a site that is faster, safer, and cheaper to run — and a staff that noticed only that publishing got quicker.

Related project

IT Step School

Written by Lasha Kakashvili — Tech Lead at Koober Coders, instructor at IT Step Academy.