From WordPress to Next.js in 5 Weeks: How We Used Claude Code as a Development Partner
TL;DR — We migrated behaimits.com from WordPress to a static Next.js export in five weeks of part-time work, spanning 53 pull requests, 49 pages, bilingual content, Azure deployment, and a full backend API rewrite. The secret wasn't using AI to write code faster — it was using AI to make good architectural decisions faster, and then encoding those decisions into reusable skills the AI could follow.
Why We Left WordPress
Our WordPress site worked. It loaded, it ranked, it handled contact forms. But it had accumulated enough pain points that a migration was increasingly hard to argue against.
Broken links with no fix. WordPress's editor is a WYSIWYG over a database, which means broken internal links — ones that the editor can't resolve because the target page no longer exists — are invisible until a visitor hits a 404. We had several that couldn't be corrected through the admin UI without direct database edits.
The bilingual admin was unusable. We run two languages — English and Czech — using a multilingual WordPress plugin. The result was an admin interface that was half English, half Czech depending on which locale a given field had last been saved in. Neither the English-speaking nor the Czech-speaking members of the team could comfortably manage it. Translations drifted, pages existed in one language but not the other, and every edit required checking two admin panels.
Plugins all the way down. A video player required a plugin that required another plugin. An embedded document viewer required a plugin that conflicted with the caching plugin. Every new feature was a negotiation with the plugin ecosystem.
Self-hosting had hidden costs. We were running a VPS with PHP, MySQL, and a reverse proxy — operational overhead for what was effectively a brochure site. The database needed backups. The server needed patching. The WordPress installation needed updates. Security meant managing that entire stack.
The source of truth was a database you couldn't diff. No version history, no CI/CD, no tests. A content change was an edit in a WYSIWYG and a click of "Update." There was no way to review it, roll it back cleanly, or know who changed what and why.
The requirements going in were clear:
- Static delivery for global performance. Our customers are spread across Europe and the US. WordPress on a single-region VPS meant every transatlantic request was hitting a PHP application server, waiting for a database query, and rendering HTML on the fly. We needed pre-rendered HTML served from a CDN edge — so the site loads fast regardless of where the visitor is.
- Everything in git. Content changes, navigation updates, new pages — all reviewable, all reversible, all attributable.
- Azure for security and delivery. We're already an Azure shop. Azure Static Web Apps with Front Door gives us edge caching, DDoS protection, and a WAF without running or patching anything ourselves.
- Content editable by non-technical staff. Pages, copy, and navigation needed to be manageable without a developer involved for every change — the one area where WordPress genuinely has the edge.
The Hypothesis: AI as Developer, Human as Architect
We'd been using Claude Code for internal tooling, and the question wasn't whether it could write code — it clearly could. The question was whether it could sustain that across a project with real scope and real structure, without going off the rails.
The answer, as the GenericPage V1 story later proved, is: yes, within a well-defined architecture. And no, if you leave it to define the architecture itself. Claude is an excellent developer. It is not a substitute for architectural thinking. The distinction matters.
The experiment was this: humans set the structure, Claude executes within it. Decisions about how to model bilingual navigation, where content lives, what a page template looks like — those were made by people. Claude's job was to do the work inside those decisions, consistently and at scale, across dozens of pages and hundreds of content files.
That's the experiment we ran for five weeks in early 2026.
The Stack
The decisions started with constraints, not preferences:
- Static export — no Node.js server in production, so Azure Static Web Apps works as a CDN with zero operational overhead
- App Router — we wanted the
[locale]dynamic segment to drive i18n, withgenerateStaticParams()prerendering bothenandcsat build time - Tailwind CSS v4 — the new
@themedirective meant we could define our brand palette once inglobals.cssand have every utility class generated automatically, no config file required - TypeScript strict mode — non-negotiable; the block-based content system we were building needed the discriminated union support to be safe
What emerged: Next.js 16 App Router, TypeScript, Tailwind CSS v4, static export to Azure Static Web Apps.
One early decision worth noting: the English-at-root URL scheme. The requirement was /integration/tibco/ not /en/integration/tibco/, but Next.js's [locale] routing naturally produces the prefix. The solution was a post-build script that copies out/en/ into out/ and rewrites all /en/ references in the HTML and RSC payload files. Once that decision was made and documented in CLAUDE.md, Claude never got it wrong again — it was simply part of the rules.
CLAUDE.md: The Living Contract
The most important file in the repository isn't a component or a page — it's CLAUDE.md.
From the first week, we maintained a running specification of every architectural decision, naming convention, and workflow in this file. Not as documentation for humans (though it serves that too), but as context Claude loaded at the start of every conversation.
A few examples of what lives there:
Content JSON filename convention for nested routes — use hyphens to flatten the path hierarchy rather than subdirectories.
/integration/opensource-middleware-integration/api-management/→integration-opensource-middleware-integration-api-management.json
Always use Tailwind utilities or CSS variables — never hardcode hex values.
The
localePathutility must be used for all locale-prefixed links in client components.
What this meant in practice: by week three, Claude never suggested hardcoded colors, never created a subdirectory for content JSON, never forgot to add generateStaticParams. The CLAUDE.md was doing active enforcement work — not through a linter, but through shared context.
The document grew organically. When Claude made a mistake, we added a rule. When a pattern emerged that we wanted to repeat, we documented it. By the end of the project, CLAUDE.md was ~400 lines of precise specification, and it was doing more quality-control work than most linters.
CLAUDE.md isn't just a project artifact — it's a living operations document. Adding a new page today, fixing a bug six months from now, onboarding a new developer — the same context that guided the migration guides every subsequent change. The rules didn't become stale once the site launched; if anything they became more important, because the migration period had someone watching every decision closely, while maintenance happens in shorter, less connected sessions. CLAUDE.md is what makes those sessions coherent.
The Block Architecture: Designing for AI Contribution
The central architectural decision of the project was GenericPageV2 — but it wasn't the first attempt.
The original GenericPage.tsx started life as a reasonable template. Claude, given the task of migrating pages, did exactly what you'd expect a capable developer to do: each time a page needed something slightly different, it added a new section component. CTASection.tsx. CardGrid.tsx. CareerApplicationForm.tsx. OpenPositions.tsx. PageHero.tsx. PageHeroSplit.tsx. These all accumulated in src/components/sections/ and were imported directly by the template. The template grew. The sections grew. The content model was inconsistent across pages because each migration was treated as a local problem.
By week three, GenericPage.tsx and its sections represented over 1,100 lines that were increasingly difficult to reason about. Adding a new page meant navigating an implicit dependency graph of sections, props, and content shapes that only made sense if you'd followed every prior migration decision. That's a maintenance problem — and it's exactly the kind of problem that doesn't surface until you're looking at the tenth page and wondering why two "hero" sections behave differently.
The team stepped back and rearchitected from scratch. The redesign had one guiding constraint: the template should be a thin dispatcher, and all layout logic should live in isolated block components. GenericPageV2.tsx became 20 lines of switch statement. Each block type got its own file in src/components/blocks/. Content was formalized into a TypeScript discriminated union in src/lib/content-v2.ts. A page was nothing more than a blocks[] array in a JSON file.
The cleanup commit deleted 1,167 lines. After that, GenericPageV2 — a block-based page template where every aspect of a page is declared in a JSON file.
{
"seo": { "title": "...", "description": "...", "path": "/page/" },
"breadcrumbs": [...],
"blocks": [
{ "type": "Hero", "variant": "split-white", "title": "...", "image": "..." },
{ "type": "ContentSection", "title": "...", "paragraphs": ["..."] },
{ "type": "CardGrid", "columns": 3, "cards": [...] },
{ "type": "CTA", "ctaText": "Contact Us", "ctaHref": "/contact-us/" }
]
}
The template itself (GenericPageV2.tsx) is a thin dispatcher — it loops over blocks[] and delegates to the appropriate renderer component. The renderers (HeroBlock.tsx, ContentSectionBlock.tsx, CardGridBlock.tsx, etc.) own all the layout logic.
Why does this design matter for AI-assisted development? Because it separates the problem of "what content goes on this page" from "how to render it." When migrating a page from WordPress, the task becomes entirely about filling in the JSON file correctly — and that's a task Claude can do reliably given the block type reference and the rendered live page.
We ended up with 22 block types, each documented in CLAUDE.md with its full field reference. Migrating a page became a predictable workflow: fetch the live WordPress page, identify which block types map to which sections, fill in the JSON. The first few pages took an hour each. By week three, simple pages took fifteen minutes.
Skills: Teaching Claude Repeatable Workflows
The most powerful concept we developed was the skill — a Markdown file in .claude/skills/ that encodes a multi-step workflow Claude can invoke by name.
We built six skills over the course of the project:
/migrate-page <path>
The workhorse. Given a path like /integration/apache-kafka/, this skill:
- Starts the local dev server if it isn't running
- Captures a full-page screenshot and all text from both the local build and the live WordPress site using headless Playwright
- Compares them across a defined checklist: H1/headings, section order, paragraph content verbatim, images, CTAs, visual layout, styling (text alignment, font colors, bold spans, supertitle labels)
- Produces a PASS / WARN / FAIL report
- If FAIL or WARN, fixes the issues directly — edits the content JSON, re-runs the capture, and verifies
One important constraint: Claude never had access to the WordPress source code, database, or admin panel. Everything was extracted from the rendered live site — screenshots, DOM text, and visible structure. This turned out to be a feature, not a limitation. It means the same approach works for any migration where source access isn't available: a competitor's site you're rebuilding, a legacy system with no living codebase, a platform you're escaping rather than forking. If it renders in a browser, it can be migrated.
The key phrase in that last step: "fixes the issues directly." The skill doesn't just diagnose — it acts. This was the difference between a report and a workflow.
The verbatim content rule in the checklist was intentional and important. Our WordPress site had 15 years of carefully written copy, some of it translated. The migration shouldn't paraphrase — it should preserve. Having that rule explicit in the skill meant Claude flagged every reworded sentence, even when the meaning was equivalent.
/validate-layout <path>
A faster, lighter version of the migration check using Chrome browser automation rather than Playwright. Used for spot-checking visual layout after making component changes.
/add-page-seo <slug>
Automates the SEO work that would otherwise be tedious:
- Reads the page's block content to understand the topic
- Fetches the live WordPress page to see its existing title/description as a reference
- Generates keyword-optimized titles (≤38 chars, since the suffix adds another 22) and descriptions (130–155 chars) for both English and Czech
- Selects an appropriate OG image from existing assets
- Writes the
seoblock into both locale content JSON files - Runs the SEO checker to verify
What made this skill particularly valuable was the constraint documentation inside it. The skill knows that TIBCO pages should mention "consulting," "20+ years," and a concrete product. It knows the Czech descriptions should use natural Czech phrasing rather than machine-translated English. That domain knowledge traveled with the skill.
/score-page <path>
Runs a Lighthouse audit and produces a color-coded score report. Used before merging any page migration to ensure we weren't regressing on performance or accessibility.
/port-svg-icon <name>
Extracts an SVG from the live WordPress site, cleans up the path data, saves it to public/images/icons/, and updates the inline React component that uses it.
/download-image <description>
Finds an image on the live site, downloads it to the correct public/images/ directory, and updates any content JSON or component that references it.
Each skill was written once when we first needed it, then refined as edge cases emerged. The iteration loop was: use the skill, notice it missed something, update the skill, continue. The skills didn't just accelerate the work — they encoded the institutional knowledge of how we wanted the migration to work, persistently, across every conversation.
The Workflow: Feature Branches at AI Speed
Our git workflow was conventional: feature branches off stage, PRs to stage, then periodic merges from stage to main for production. What was unconventional was the cadence.
At AI-assisted speed, a feature branch might represent a complete page migration — content, SEO, layout validation, link checking — completed in a single session. We merged 53 pull requests in five weeks. That's roughly 1.5 PRs per day, including weekends.
Each PR ran through GitHub Actions CI:
npm test— Jest unit testsnpm run build— static export withROBOTS_NOINDEX=truenpm run check-links:internal— validates everyhrefin every generated HTML file
That last step caught a class of bug that would otherwise have been invisible: content JSON files that included a locale prefix in their href fields, causing double-prefixing when the block renderer prepended /${locale}. The link checker made this a hard CI failure rather than a silent runtime bug.
The Challenges Claude Code Couldn't Hide From
Not everything went smoothly. A few honest observations:
AI optimizes locally; humans need to think globally. The GenericPage V1 story is the clearest example of this. Claude was never wrong on any individual migration — each section component it added was a reasonable solution to the problem in front of it. But no single conversation held the full picture of where the template was heading. The accumulation of locally correct decisions produced a globally unmaintainable structure. Catching that required a human to step back, look at the whole, and redesign. The lesson: AI handles execution well at the task level; architectural coherence across the full project still needs human oversight.
Context window management matters. Large migration sessions — multiple pages, multiple skills invoked — would occasionally lose track of earlier decisions. The CLAUDE.md was the mitigation, but there were still moments where a freshly started conversation made a choice that contradicted something we'd decided three weeks earlier. The solution was vigilant CLAUDE.md maintenance: if a mistake was made, we turned the correction into a rule.
The i18n complexity was real. The localePath utility for client components (preserves /en/ in dev, strips it in production) was a genuine footgun. It took two or three incidents of broken links in the static build before the CLAUDE.md rule was strong enough to prevent them. The constraint — "client components must use localePath, server components don't need it" — is conceptually simple, but applying it consistently required the rule to be explicit and prominent.
Verbatim content required active enforcement. The /migrate-page skill checked for verbatim content, but early in the project — before that rule was sharp — some pages had been migrated with paraphrased copy. We ran a retrospective pass on those pages once the skill was mature. The lesson: write the skill before migrating, not after.
The Numbers
Five weeks — entirely part-time, alongside normal client work.
- 53 pull requests merged
- 49 page routes built
- 90 content JSON files (45 pages × 2 languages)
- 18 blog posts migrated (9 per language, Markdown)
- 22 block component types
- 6 Claude Code skills developed
- 4 Azure Functions written (contact form, career application, CAPTCHA challenge, email)
- 2 languages — English at root (
/integration/), Czech prefixed (/cs/integration/) - 3 automated validation scripts — link checker, SEO checker, sitemap generator
- 12 Jest test suites, 419 unit tests — components, content structure, navigation, utilities
- 3 Playwright e2e test files, 52 tests — mobile navigation, layout, and component behaviour
The site went from a WordPress installation with no version history to a fully static, bilingual, tested, CI/CD-deployed Next.js site with automated SEO validation, broken link detection, and complete content traceability through git.
And the operational cost? It went down. Azure Static Web Apps with Azure Front Door replaced a self-hosted VPS running PHP, MySQL, and a reverse proxy. No database to back up, no WordPress installation to patch, no plugin vulnerabilities to monitor. Front Door provides DDoS protection, WAF, and global CDN out of the box. The infrastructure is simpler, cheaper, and more secure than what it replaced.
What We'd Do Differently
Start with CLAUDE.md. We grew it organically, which meant the first two weeks had more mistakes than the last three. If we ran this project again, we'd spend a day before the first commit writing the architecture spec — stack choices, naming conventions, content structure, workflow rules — and treating that as a living document from day one.
Build the skills earlier. The /migrate-page skill was created around week three. The pages migrated before that needed retroactive quality checks. Skills should be created the first time you perform a repeatable workflow, not after you've done it ten times.
Automate the Czech translations earlier. The bilingual content system was architecturally solid from the start, but the actual Czech content often lagged behind the English because the translation workflow wasn't formalized. A /translate-page skill would have been worth the investment.
The Real Lesson
The most important thing we learned wasn't about Next.js or TypeScript or Tailwind. It was about how to work with AI on long-running projects.
AI-assisted development fails when you treat the AI as a smart autocomplete — ask a question, get code, paste it, move on. It succeeds when you treat it as a partner who needs to be brought into the shared context of the project: what decisions have been made, why they were made, and what good work looks like in this specific codebase.
CLAUDE.md and the skills system were our answer to that challenge. The CLAUDE.md said "here is everything you need to know about this project." The skills said "here is exactly how we do this class of task." Together, they turned a five-week migration into something that felt, by the end, almost routine — not because the problems were easy, but because the framework for solving them was clear.
That framework is now part of the codebase. Future engineers (human or AI) who open this repository find not just the code, but the reasoning behind it.
If you're considering a similar migration and want to talk through the approach, get in touch.
This article was written by Claude. I opened the project in Claude Code and said "write a blog post about this." Claude read the git history, explored the codebase, and produced the draft. No outline, no briefing, no source material handed over — just the repo. Claude had been writing content files in this repo for five weeks. It already knew the format. The project documented itself.