From Static WordPress to Next.js and MDX: An AI-Assisted Migration with Claude, GPT and Superpowers
How we migrated this site with AI-assisted development, preserved its content, and applied lessons from the Otari technical resource.
Chris Baran
· 14 min read
On this page
- Before and after
- Give content an explicit contract
- Use a workflow that leaves reviewable decisions
- Preserve content, not just file counts
- Test the behaviour that actually matters
- Beyond migration: what the Otari build made possible
- Reduce the exposed application surface
- Keep dynamic work in small, scoped Lambda functions
- Deliver a fast browsing experience from the edge
- Treat deployment as part of the migration
- Make the next change easier to verify
The original Static WordPress with CloudFront article described the previous version of this site. WordPress supplied the authoring environment, Simply Static produced the export, and GitLab CI deployed those files to Amazon S3 and CloudFront.
This follow-up explains the next step: replacing the exported WordPress source with a Next.js application and MDX content that developers and AI agents can edit directly. The AWS delivery foundation remains, with infrastructure and pipeline changes to support the new build.
Claude and GPT models participated across the development process. The Superpowers plugin provided a workflow for specifying, planning, implementing and reviewing changes. The useful detail is how that workflow interacted with the repository, the migration evidence and human decisions.
Before and after
The previous content path was WordPress → Simply Static → exported files → GitLab deployment → S3 and CloudFront.
The new path is MDX and React source → Next.js static build → GitLab deployment → S3 and CloudFront.
The distinction matters when making a change. Instead of editing an exported page or returning to WordPress to regenerate it, we can work on an article in content/insights/, a shared component in components/, or a route in app/. Infrastructure and pipeline definitions are versioned alongside that source.
Next.js is configured for static export. It produces out/, including HTML and browser assets, for deployment. There is no Next.js application server handling each page request, and no runtime LLM is required to read the site. Interactive browser components can still operate on those static pages.
The static-export settings are explicit in the application configuration:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
trailingSlash: true,
images: { unoptimized: true },
agentRules: false,
};
export default nextConfig;output: "export" produces deployable files in out/. Trailing slashes match the directory-index routing at CloudFront. Images are served without a request-time Next.js image optimiser; image preparation happens separately.
We call this source arrangement “LLM-native”: readable, structured files with explicit rules and checks. That description does not make the repository automatically understandable or the generated changes automatically correct.
Give content an explicit contract
Each Insight has an index.mdx and a line-art cover.svg. Front matter supplies the title, summary, dates, topic, format and service relationships. MDX provides prose and selected reusable components without duplicating the article layout.
The content loader validates that structure. Dates must be quoted strings. Unknown front-matter fields fail validation, so a typo such as drafts cannot silently stand in for draft. References to services and related articles must resolve. Covers also have structural checks, including a standard viewBox and a text label.
Drafts use draft: true. They can be previewed in development, while production listings and generated article paths exclude them. The build also prunes their designated media folders from the export. Those controls are useful publishing boundaries; they do not make a public repository a place for confidential drafts.
Repository instructions document conventions that an agent needs to preserve, including typography, heading style and content restrictions. Some are checked automatically; others still need review. Keeping that distinction explicit avoids mistaking a written rule for an enforced one.
Use a workflow that leaves reviewable decisions
Superpowers structured the work into an agreed design, implementation plans, scoped tasks, review and verification. Those artefacts gave agents a concrete contract and gave the owner something more useful to assess than a large unexplained diff.
Explore Superpowers on GitHub →
Bounded autonomy for more than 10 hours
With that structure in place, we found the LLM could work autonomously for more than 10 hours and produce highly accurate results. This is our experience of the workflow, rather than a controlled benchmark or a guarantee for every task.
The boundaries mattered: the specification described the intended result, the plan divided the work, and review and verification supplied checks against the implementation. That allowed the agent to carry a substantial body of work forward without a person directing every edit. Human decisions still set the scope and determined whether the result was acceptable to publish.
Claude and GPT supported the project across its development history. We do not attribute every component or correction to a particular model here. The durable record is the source, design documents, changes and verification evidence.
Human feedback determined whether the design served a marketing site, which content changes were acceptable and when to deploy. Agent review helped find problems, but agreement between models was never sufficient evidence on its own. We still needed checks against source material and the rendered pages.
The repository also acted as the handover between sessions and tools. A useful handover identifies the agreed scope, current state, decisions, unfinished work and commands used to verify it. That makes a change of model or development environment manageable without assuming that a new session remembers the earlier conversation. Credentials and private operational details do not belong in a public handover document.
Preserve content, not just file counts
The migration covered eight articles as well as the About and legal pages. The captured website supplied the source material. Extracting text programmatically helped preserve curly punctuation and avoid retyping errors.
We compared the captured content with the rendered output and documented intentional differences. These included duplicate title text and a damaged article that contained a pasted copy of another post. Removing that pasted section required an explicit decision; the migration was not permission to rewrite the remaining article.
The parity report used a word-multiset comparison to count lost and added words. That method has an important limit: the same words in a different order still match. Paragraph-level comparisons were needed to establish sequence and wording, alongside browser checks for presentation.
Rendering could introduce subtler differences. Three numbered statements became separate Markdown lists until their periods were escaped. A paragraph with touching emphasis spans needed explicit markup to preserve the source formatting. These issues would be easy to miss if acceptance stopped at “the MDX compiles”.
Test the behaviour that actually matters
One implementation omitted the About page’s “Simplify. Scale. Succeed.” labels. Review restored them, then a specific regression check was added. Removing “Scale.” deliberately caused that check to fail, providing evidence that it guarded the missing element.
Other checks covered content validation, article routes, feed entries, sitemap URLs and share images. These answer different questions from a browser review. A route can exist while its mobile layout overflows; content can be present while its hierarchy makes the page hard to follow.
The visual work needed another feedback loop. The owner compared the new pages with the original marketing site and directed changes to the homepage, About layout and hover treatments. Preserving the purpose of the site required more than moving its words.
Beyond migration: what the Otari build made possible
The build-out of otarimx5050.com shows how a related Next.js and MDX approach can support a specialised technical resource. It is a separate project with its own components and content model; the features below are not all present on this business site.
The site was also the basis for practical restoration work. With limited formal training and study in electrical engineering, I used the resource and its underlying AI-assisted research to complete a full recap and calibration of the recorder without an expert guiding the work. Developing the website helped organise the material I needed to understand and apply: manuals, diagrams, component explanations and restoration notes.
This gives the project a concrete outcome beyond publishing content. It demonstrates my experience of using AI to work into an unfamiliar technical domain and carry out a demanding task. The software features below helped make that knowledge usable.
None of the copy on the Otari site was edited directly in an editor. We reviewed the output and returned prompts requesting specific revisions, which the LLM then applied. The workflow included human editorial feedback without manual text editing. For the digital editions, the task was to preserve and structure existing source text; for explanatory material, it included developing the copy. That distinction matters when assessing the result: faithful transcription and a sound technical explanation require different checks.
Digital editions use a reusable publishing structure. An edition directory contains an edition.json manifest and section MDX files. Generic routes render the edition and its sections. This lets additional manuals and brochures use an established structure rather than requiring a new route implementation for every document.
Diagrams retain a route back to the source. The image viewer supports zoom and pan, with a switch between a vector redraw and the original scan where both exist. Readers can inspect a clearer drawing and compare it with the source. The comparison supports verification; it does not prove the redraw is correct.
Annotations distinguish transcription from correction. A source-note component preserves the printed wording and offers an explanation with a crop from the scan. This avoids silently presenting an editorial correction as original text. The library provides examples of the digital material.
MDX can host interactive teaching components. The Inside the Audio page includes a clickable signal-path explorer and a capacitor demonstration with a slider. React components handle the interaction, embedded alongside the explanation. This makes a static site capable of richer learning experiences without turning every page into a server-rendered application.
Search is a build artefact. Pagefind indexes the Otari export after the site build, and the browser uses that index for search. The index must be rebuilt when the content changes. This capability belongs to the Otari site; the Expert Cloud & AI site does not currently offer search.
Together these features suggest a useful direction for document modernisation: structured editions, reusable interactions and visible source evidence. Applying that pattern to an enterprise library would also require decisions about permissions, source rights, document versions and specialist review.
The broader connection between the projects is architectural: both put content and presentation into source that can be inspected, changed and rebuilt. It is possible to reuse that approach without pretending that a marketing site and a restoration archive need the same features or the same editorial workflow.
Reduce the exposed application surface
Static delivery is also a security architecture decision. A normal page request retrieves built files through CloudFront from S3. It does not invoke a CMS, query an application database or execute a Next.js page handler. Removing those request-time components substantially reduces the application attack surface compared with an equivalent dynamically hosted site.
The origin is a private S3 bucket with public access blocked. CloudFront uses Origin Access Control to read it; we use the S3 regional endpoint rather than a publicly accessible S3 website endpoint. Viewers receive the site through CloudFront, and the content delivery behaviour permits GET and HEAD. This follows AWS’s guidance for restricting access to an S3 origin.
This excerpt connects CloudFront to the regional S3 origin through the separately defined access control:
Origins:
- DomainName: !GetAtt WebsiteBucket.RegionalDomainName
Id: S3Origin
OriginAccessControlId: !GetAtt CloudFrontOAC.Id
S3OriginConfig: {}The bucket policy grants s3:GetObject to the CloudFront service only when the source is this distribution. Together with S3 Block Public Access, that keeps the bucket from becoming an alternate public delivery path. This is an excerpt from the distribution, not a complete CloudFormation template.
Otari’s browser-side search extends that approach. Pagefind produces a static index at build time, and search runs in the browser against downloaded index files. There is no search API or query-processing server for a visitor to attack. The index is public content, so it must only contain material intended for publication.
Attack surface and blast radius are related, but distinct. Static delivery removes several entry points; restricted permissions limit what a compromised component can reach. Deployment credentials and the build pipeline still control what visitors receive, and shipped JavaScript still needs review. Those remain important boundaries even when there is no general-purpose application server behind the site.
Keep dynamic work in small, scoped Lambda functions
Contact handling and traffic processing are separate modules with defined inputs and AWS permissions. They do not sit in the request path for ordinary browsing or Otari’s search.
The contact handler accepts a structured submission, checks the request method and body size, validates fields, applies a honeypot check and verifies Turnstile before sending mail through SES. Its permissions are scoped to its required resources and configured mail destination. The endpoint remains an exposed interface that needs validation and abuse controls; separating it from content delivery makes that interface smaller and easier to reason about.
For example, the handler rejects oversized bodies and malformed JSON before proceeding to field validation and Turnstile:
const raw = event.isBase64Encoded
? Buffer.from(event.body ?? "", "base64").toString("utf8")
: (event.body ?? "");
if (Buffer.byteLength(raw, "utf8") > MAX_BODY_BYTES) return respond(413, { ok: false, error: "too-large" });
let data: Record<string, unknown>;
try {
const parsed: unknown = JSON.parse(raw);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object");
data = parsed as Record<string, unknown>;
} catch {
return respond(400, { ok: false, error: "json" });
}The constants and respond helper are defined elsewhere in the handler module. This is one validation stage; valid JSON alone does not authorise a submission or cause an email to be sent.
Traffic processing works differently. A scheduled Lambda reads CloudFront logs, writes reports and publishes an aggregate JSON file that the dashboard can read. Visitors do not need access to the log-processing function or raw logs. Its role is scoped to the relevant log and report prefixes, with public output confined to the site’s data prefix. This limits the resources that function can affect if compromised.
The traffic function’s public-write permission illustrates the boundary:
- Sid: WritePublicAggregate
Effect: Allow
Action: 's3:PutObject'
Resource: !Sub 'arn:${AWS::Partition}:s3:::${SiteBucket}/data/*'This statement grants writes to the data/ prefix, rather than to the site’s HTML or scripts. The function has separate permissions for reading logs and writing private reports. Assessing its total reach requires reviewing the complete role; this excerpt shows the public-output permission only.
Using S3, CloudFront and managed Lambda runtimes removes the need to operate and patch a website host’s OS, web server or CMS middleware. AWS manages the underlying infrastructure and provides managed-runtime updates. We retain responsibility for application code, bundled dependencies, IAM, configuration, and moving to supported runtimes. AWS documents that division in its Lambda runtime shared-responsibility guidance.
The resulting operational model is a static delivery path plus a few narrow service interactions, each with a specific purpose and permission boundary.
Deliver a fast browsing experience from the edge
The same architecture also supports fast browsing. Pages are rendered at build time, and CloudFront can serve cached HTML, images, styles and scripts from edge locations close to readers. A cache hit avoids both a trip to the S3 origin and any application rendering or database work. AWS describes the request flow in how CloudFront delivers content.
Our distribution enables compression and does not forward viewer cookies or query strings to the static origin, allowing requests to share cached objects. The deployment pipeline gives fingerprinted Next.js assets long-lived immutable caching. Content updates are handled through the deployment and invalidation process so that caching and publishing work together.
The fingerprinted asset upload sets the caching policy explicitly. This excerpt is line-wrapped from the pipeline; $S3_BUCKET is resolved earlier in the job:
aws s3 sync out/_next/static/ "s3://$S3_BUCKET/_next/static/" \
--delete \
--cache-control "public, max-age=31536000, immutable"The one-year policy is for content-hashed assets, whose filenames change when their contents change. The pipeline uploads them before the HTML that references them, then deploys the remaining files and invalidates CloudFront. HTML does not receive this immutable policy. Here, --delete removes stale objects within the target asset prefix; this command is a deployment excerpt, not a standalone publishing recipe.
That is an effective foundation for a responsive global site: prebuilt content, reusable edge-cache entries and S3 as the origin. Actual latency still depends on cache warmth, the reader’s connection and the assets a page loads. We have not published a global latency benchmark for this migration.
A first measured sample
On 22 September 2026, we tested the live homepage at https://expertcloudandai.com/ using WebPageTest’s free instant tests. Both reports used desktop Chrome v148 and the WiFi profile: 240/120 Mbps, 2 ms RTT. The results below are one completed run per location, not averages or percentiles.
| Test location | Time to first byte | Largest Contentful Paint | Layout shift (CLS) | Total time reported |
|---|---|---|---|---|
| Council Bluffs, Iowa, USA | 118 ms | 0.571 s | 0 | 0.751 s |
| Amsterdam, Netherlands | 152 ms | 1.804 s | 0 | 9.315 s |
Both runs reported zero Total Blocking Time. The browser received the first byte in under 160 ms and reached Largest Contentful Paint in under two seconds in these two samples. That is useful initial evidence of responsive delivery from geographically separated locations.
The Amsterdam report also shows why those metrics should be distinguished: its total time was 9.315 seconds even though its largest content painted at 1.804 seconds. Fast initial delivery does not mean every resource has finished loading. The cause of that longer tail has not been established by this sample.
These are fast desktop-network tests of the homepage, not mobile results, measurements of this draft article, or a global service guarantee. We did not control or verify CloudFront cache warmth, so the figures cannot be labelled cold-cache or warm-cache results. An Asia attempt reached the service’s anonymous free-run limit and produced no measurement. The linked reports retain the test conditions and results; repeated runs across more regions would be needed for a broader benchmark.
Treat deployment as part of the migration
The new pipeline builds the site before deploying out/. CloudFront routing handles canonical hostnames, legacy redirects and directory indexes. Unknown pages return an actual 404 rather than the old homepage with a successful status.
Cutover needed coordination: redirects pointing to new pages are only useful once those pages exist in the deployed build. The runbook records the deployment sequence, checks and rollback options. Reversing a source change alone may not reverse infrastructure or routing changes, so recovery needs to account for those separately.
Dynamic services also remain separate from the static Next.js export. The contact implementation uses an AWS Lambda endpoint, not a Next.js server route. At the time of writing, the public form remains disabled pending its endpoint and Turnstile site-key configuration. Shipping the website is distinct from accepting every supporting service as operational.
Make the next change easier to verify
The result is an editable source with a repeatable build and explicit checks. A future change can start with its intended behaviour, touch the relevant content or component, and be reviewed against both automated evidence and the browser result.
That is the useful connection between AI-assisted migration and ongoing software development: preserve what matters, make the source understandable, and leave the next developer a way to verify their work.
For the decision-maker’s perspective, read a practical approach to enterprise AI delivery. To explore the approach for an existing system, discuss an AI-assisted migration assessment.
