I purchased the domain evidenceekanem.me and hosting from GoDaddy on the same day in March 2024. By June 21, 2024, I'd paid a designer to create a complete Figma design: light mode, dark mode, every section pixel-perfect down to the Poppins font and that cyan #67F4FF accent color I loved.
Then I did absolutely nothing with it for months.
If you're a developer who's been "about to build" your personal site for a while now, this post is for you. I'm going to walk you through the entire journey, the tech decisions I made, the engineering problems I solved, and how I finally shipped it.
The Backstory
I've been a full-stack developer for over seven years, building production systems in C#/.NET, Laravel, WordPress, and Vue.js. I've shipped mobile apps with NativeScript, enterprise dashboards with Vuetify, backend services running on Kubernetes. I've solved complex problems for clients and employers across multiple stacks and platforms.
But building something for yourself hits different. There's no deadline, no client, no sprint review. Every week I told myself I'd start next weekend. Next weekend came, and I told myself the same thing again.
I even went ahead one weekend and built the header and hero section of the landing page. It looked good. I pushed it to GitHub, closed my laptop, and went right back to procrastinating. That header and hero section sat in a repo collecting dust for months. The domain sat there. The paid Figma design sat there.
Since 2021, I've been documenting every code hurdle I resolve in my MacBook Notes app. Debugging tips, workarounds, patterns I figured out the hard way. I always told myself I'd write proper blog posts about them someday. I even wrote a short article on Medium back in 2022 about the new way of defining Eloquent accessors and mutators in Laravel 9.x. That was supposed to be the start of a regular writing habit. It wasn't.
In 2025, I renewed the domain and hosting. Let that sink in for a second. I paid for another year of a domain that had nothing on it.
What finally broke the cycle was something I kept running into: people would check out my work, want to learn more about me, and I'd end up sending them my Upwork profile or LinkedIn page as a makeshift portfolio. It worked, but it wasn't mine. I had no central place that told my story on my terms. After seven years of building digital products for everyone else, I didn't have one for myself. It was time to change that.
The Tech Stack (And Why I Changed It)
My original plan was to build the site with Vue.js and Laravel on the GoDaddy shared hosting I'd already paid for. That was a comfortable stack for me. But as I thought about what I actually wanted this site to be, the plan evolved.
This wasn't going to be a static portfolio with a contact form. I wanted a full-featured blog with an admin panel, automated cross-posting to multiple platforms, AI-powered content rewriting, self-hosted analytics, and a CI/CD pipeline that deploys on every push. Shared hosting wasn't going to cut it, and I wanted the architecture to reflect the kind of systems I build professionally.
I ended up going with Nuxt 3 and C#/.NET, hosted on a proper VPS. Here's the final stack:
Backend:
- ASP.NET Core 8 with Clean Architecture (four separate projects: Web API, Core, Infrastructure, and Tests)
- Entity Framework Core with PostgreSQL 16
- JWT authentication with refresh token rotation
- AWS S3 for image uploads
- Resend for contact form emails
- Claude API (Anthropic's Sonnet model) for AI-powered content rewriting
- xUnit for backend tests
Frontend:
- Nuxt 3 with Vuetify
- TypeScript, Pinia for state management
- TipTap as the rich text editor for the admin panel
- Server-side rendering for SEO, client-only rendering for the admin section
- Vitest for frontend tests
Infrastructure:
- Hetzner VPS at €6/month. The backend, frontend, Umami analytics, PostgreSQL, and Caddy all run on it comfortably
- Docker Compose orchestrating everything
- GitHub Actions for CI/CD: push to
master, and it builds, pushes to GHCR, SSHs into the server, and deploys - Caddy handles automatic HTTPS with Let's Encrypt
The Cross-Posting Pipeline: This is probably the most architecturally interesting part. When I publish a blog post, the system automatically rewrites it for different platforms using Claude's API. A professional version for LinkedIn, a thread format for X/Twitter, a technical version for Dev.to, and an article for Hashnode. Each platform gets its own tailored content with a canonical backlink to my site. Every cross-post attempt is tracked independently, so if LinkedIn succeeds but Dev.to fails due to a rate limit, I can retry just the failed one from the admin panel. The AI-generated content is saved on first attempt, so retries don't burn extra API credits.
The Engineering Decisions (And Problems I Solved)
Choosing Nuxt Over Plain Vue for SEO
My original frontend was a Vue 3 SPA with Vuetify. I'd already built the components, themed them to match my Figma design, got the dark mode toggle working. It looked great.
But I knew from experience that a blog lives and dies by search engine traffic. With a standard Vue SPA, search engine crawlers see an empty <div id="app"> and have to execute JavaScript to render your content. Google's crawler can handle this sometimes, but it's inconsistent. Bing and DuckDuckGo struggle even more.
For a portfolio, you might get away with it. For a blog where organic search traffic matters, it's a risk I wasn't willing to take.
I made the call to migrate to Nuxt 3. Blog posts get pre-rendered as static HTML at build time for perfect SEO, while the admin panel stays as a client-only SPA since it doesn't need indexing. The migration was mostly structural: file-based routing instead of Vue Router config, useAsyncData and useFetch for data loading. The components, Pinia stores, and composables all carried over cleanly.
Why I Walked Away from Nuxt 4
Before settling on Nuxt 3, I actually tried migrating to Nuxt 4 first. It was the latest version, and I figured I'd future-proof the project.
That decision cost me time I didn't need to spend.
The Vuetify color overrides stopped working. My cyan accent rendered as the default Vuetify purple. The dark mode background ignored my custom #060824 value entirely. Then the dev server started crashing with IPC connection closed errors deep in Nuxt's internal vite-node bridge.
I stepped back and made a pragmatic call: revert to Nuxt 3. The color overrides worked immediately. The dev server was stable. The vuetify-nuxt-module handled theming through nuxt.config.ts without issues. In production work, stability beats novelty every time. I apply the same principle to client projects, and my own site shouldn't be any different.
Translating a Paid Design into Pixel-Perfect Code
I'd had the Figma design since June 2024, paid for and delivered by a designer, with dark and light mode screens and a very specific color palette. Translating that into code is one of those tasks that sounds simple until you're actually doing it.
The first pass looked "close enough" in the way that immediately tells you it's not right. The hero section was using a purple accent where it should have been cyan. Section titles in dark mode were plain white instead of the #67F4FF that gave them that electric pop. The dark background was #0D0D1A when the design specified #060824. The skill chips were the wrong size. The card shadows had the wrong opacity values.
I went back to the Figma Dev Mode, exported every CSS value I could, and corrected each discrepancy:
- Hero name/title:
#67F4FF(cyan, not purple) - Dark mode background:
#060824 - Light mode background:
#FFFFFF - Card shadows:
rgba(196,196,196,0.10)for light,rgba(0,0,0,0.10)for dark - Skill chips:
#080B34at exactly 161×59px
This kind of attention to detail is what separates a polished product from a "developer-built" one. I hold my own work to the same standard I hold client deliverables.
Adapting When Medium Killed Their API
My original cross-posting pipeline included Medium as one of the target platforms. Then I discovered that Medium stopped issuing new API integration tokens in January 2025. No new tokens means no automated posting.
Rather than waste time looking for workarounds, I pivoted to Hashnode. Hashnode has an active GraphQL API and actually encourages developers to integrate with their platform. The swap was clean: I commented out the Medium publisher code (didn't delete it, in case Medium reverses course someday), added a Hashnode publisher, and updated the AI prompt to format content for Hashnode's audience. The whole change took less than a day.
Solving EF Core Migrations in a Dockerized Pipeline
This was a problem I expected to be straightforward but required some careful thinking.
The goal: automatically run Entity Framework Core database migrations as part of the CI/CD pipeline. The problem: my API container is built from mcr.microsoft.com/dotnet/aspnet:8.0, the runtime image. It doesn't include the .NET SDK, the dotnet ef CLI tool, or the source code. EF Core migrations need all three to run.
The solution I went with was to build a completely separate Docker image specifically for running migrations. A Dockerfile.migrate that uses the full .NET SDK image instead of the runtime image, copies in the entire source code (including migration files), installs dotnet-ef globally, and has a single job: run dotnet ef database update and exit. No web server, no background services, just the migration and done.
A separate docker-compose.migrate.yml defines the migrate service, with depends_on: db: condition: service_healthy to ensure Postgres is ready before the migration attempts to connect. In the GitHub Actions workflow, both compose files get copied to the server and the migration step runs them together so the migrate service has access to the shared database network.
The key architectural principle here: EF Core migrations are a build-time/source-time concern, not a runtime concern. Keeping the migration runner separate from the production image is cleaner, safer, and follows the same separation of concerns I apply to the application code itself.
Resolving the Umami Routing Conflict
Umami's admin panel lives at /admin, and so does my blog's admin panel. Putting both behind the same domain meant they were fighting over the same route in the Caddy config.
The cleanest solution was to give Umami its own subdomain: analytics.evidenceekanem.me. Caddy provisions a separate SSL certificate for it automatically. My blog admin lives at evidenceekanem.me/admin, Umami lives at analytics.evidenceekanem.me. No path-prefix hacks, no routing conflicts, clean separation.
What the Site Does Today
Here's what's running on that €6/month Hetzner box:
- Portfolio and blog at evidenceekanem.me with server-rendered pages for SEO
- Admin panel where I write posts using a TipTap rich text editor, upload cover images to S3, and publish with one click
- Automated cross-posting to LinkedIn, X/Twitter, Dev.to, and Hashnode, each with AI-tailored content and canonical backlinks
- Contact form that sends email notifications via Resend, with a smart Reply-To header so I can respond directly to the sender
- Self-hosted analytics via Umami at analytics.evidenceekanem.me: privacy-friendly, no cookie banners, tracks page views and referrer sources so I can see how much traffic the cross-posted content drives back
- Automatic HTTPS via Caddy with Let's Encrypt certificates
- CI/CD: push to master, and the whole thing rebuilds and redeploys within minutes
What I'd Do Differently
I'd start with Nuxt from day one. Building the frontend in Vue first and then migrating to Nuxt was rework I could have avoided. If your project involves a blog or any content that needs SEO, go straight to Nuxt (or Next.js if you're in the React world).
I'd skip the Nuxt 4 detour. Using beta frameworks for a production project is almost always a mistake unless you're prepared to debug framework internals. Stick with what's stable.
I would have started sooner. This is the biggest one. The gap between having the design (June 2024) and shipping the site was way too long. The project wasn't nearly as daunting as I'd built it up to be in my head. Most of the actual building happened in concentrated bursts. Once I started, the momentum carried me.
If You're Procrastinating on Your Own Site
Here's what I'll tell you: your personal website doesn't need to be perfect on launch day. Mine still has things I want to tweak and features I want to add. But it's live. It's serving real pages to real people. And every time I send someone a link to evidenceekanem.me, it feels a lot better than having nothing to show.
Stop designing it in your head. Stop researching the perfect stack. Stop telling yourself you'll start next weekend.
Pick a weekend. Open your editor. Write the first line of code.
The domain is already paid for. The design is already done. The only thing left is you.
This is my first blog post on evidenceekanem.me. If you're a developer building your own site, I'd love to hear about your journey. Reach out via the contact form or find me on LinkedIn.
Top comments (0)