Portfolio Headless Migration
By decoupling the frontend from the backend and moving to a fully static architecture, I was able to eliminate server-side security vulnerabilities while drastically improving site performance and developer experience.
The Challenge
For years, since graduating way back when, my personal site ran on WordPress. Wordpress is the giant of the internet for a reason, offering an easy ecosystem where you don't have to think too hard to get a post live. But as a developer, "fine" usually doesn't cut it for long. I wanted to build things my way, not the WordPress way. I felt like I was wearing oven mitts while trying to code, restricted by a guided process that hid the mechanics I wanted to control.
There were two major catalysts that pushed this from a "nice to have" to a necessity.
First, I was getting genuinely concerned about security. I started noticing a significant influx of bot traffic coming from Singapore and China in my Google Analytics. This is a reported issue for many WordPress sites right now, where bots probe PHP endpoints for vulnerabilities. Even with security plugins, seeing that activity spike made me nervous. I realized that as long as I was running a dynamic PHP application with a database exposed to the web, the risk would never be zero.
Second, I looked at my content strategy. I wasn't using any User Generated Content. No comments, no forums, no user logins. Why was I rendering pages on the server for every single request when the content wasn't changing? It was inefficient. I wanted the speed optimization that comes with serving raw, pre-built HTML files.
I needed a stack that would let me use the modern tools I work with daily—Next.js and TypeScript—while giving me a headless CMS that I could self-host without the bloat.
The Solution
I decided to tear it all down and go headless. I chose Next.js for the frontend to leverage Static Site Generation (SSG) and Strapi for the CMS because I wanted that API-first flexibility.
The goal was to reach a state of "Static Export." When you visit the site now, you aren't asking a server to build a page or query a database. You are simply downloading a pre-made HTML file. This makes the site infinitely faster and effectively unhackable since there is no backend logic running on the public-facing server.
However, moving from a monolithic ecosystem like WordPress to a decoupled architecture introduced several technical hurdles that required some custom engineering.
Solving for Static
In WordPress, a URL exists simply because a database entry exists. In a Static Site Generator, the build process needs to know explicitly which pages to create. If I didn't tell Next.js about a specific blog post, it wouldn't generate the file.
I utilized the generateStaticParams function in the Next.js App Router to bridge this gap. Before the build begins, this function fires an API call to Strapi and requests the slugs for every published project and blog post. It returns that list to Next.js, which then iterates through the array and generates a static HTML file for every single slug it received.
Solving for CMS Images
One of the first issues I hit was image management. I host Strapi locally on my development machine to keep costs down and simplicity up. But this created a gap during the build process. When Next.js runs its build command, it has no idea that my images exist inside the local Strapi folder. The build would succeed, but the final output would have broken image links.
To fix this, I wrote a custom Download Images script. I created a .mjs file that runs automatically before the Next.js build command, using the pre function of npm. It looks at my local Strapi uploads directory and mirrors the entire contents directly into the Next.js out/uploads folder. This ensures that when the static HTML is generated, the image paths align perfectly with the files in the deployment bundle.
Solving Redirects and 404
In my need for simplicity of uploading these static pages I wanted to have the system generate a .htaccess file where I have redirects from the old site to the new and set my 404 page. I had to create another .mjs file that generates the .htaccess file. it's pre-populated with an array of old wordpress urls and their new location in this Static Site generator
Project Key Features
- Next.js Static Export: Utilized output: 'export' to generate a purely static site, eliminating server-side processing and database vulnerability.
- Custom Asset Syncing: Engineered a pre-build script to mirror local CMS assets to the production build folder automatically.
- Dynamic Server Configuration: Created a module to programmatically generate .htaccess files for redirects and caching headers during the build.
- Automated Route Generation: implemented generateStaticParams to fetch CMS slugs and dynamically build all site routes.
- Component-Based Content: Replaced standard WYSIWYG editing with Strapi Dynamic Zones to allow for complex, custom-styled layouts within blog posts.
This migration wasn't just about changing technology; it was about regaining ownership of the platform. By moving to a custom static architecture, I traded the convenience of WordPress for the absolute control of a modern stack. The result is a site that is significantly faster, impervious to the security threats plaguing PHP sites and NextJS (just so happened CVE-2025-55182 was published today), and built exactly the way I want it.