I Rebuilt My Portfolio with Astro (and an AI Did Most of the Work)

How I migrated k007sam.com from a jQuery HTML5UP template to Astro 4 + Tailwind in a single session — and what it taught me about AI-assisted development.

astroai-developmentwebtailwinddevlog

Let me be real with you — my old portfolio was embarrassing for someone who calls himself a platform architect.

Single index.html. jQuery. An HTML5UP template from 2018. SCSS compiled by hand. The Docker image just… served raw HTML. No build pipeline. No component model. No blog. Editing the contact section meant Ctrl+F-ing through 800 lines of markup and hoping for the best.

I finally snapped. Here’s how I rebuilt the whole thing in one session, with Claude doing the heavy lifting while I stayed in the driver’s seat.


Why Astro?

I had three hard constraints:

  1. Static output only — this site runs in a 0.5 CPU / 256MB container. No SSR, no serverless, no runtime surprises.
  2. Existing sub-apps stay untouched/mealplanner, /cah, /wordwiz are self-contained tools. Zero migration cost or I’m not doing it.
  3. I want a blog — markdown files, no CMS, no database.

Astro hits all three. Static by default, public/ folder passes files through untouched, and Content Collections makes the blog trivial. Next.js felt like overkill. SvelteKit is great but I didn’t want to learn a new component model mid-migration. Astro’s .astro files are close enough to HTML that I can read and edit them without a tutorial.


Phase 1: Scaffold

Started with a gameplan doc before touching a single file. This is a habit I’ve developed — align on the plan first, then execute. Saved us from two potential wrong turns.

The scaffold came out clean:

  • src/layouts/BaseLayout.astro — one shell: fonts, analytics, favicon, <slot />
  • 6 section components: Hero, Intro, Skills, Experience, Projects, Contact
  • Data in JSON files (skills.json, experience.json, projects.json) — content is now editable without touching markup. This alone was worth the migration.
  • Blog via Content Collections: drop a .md file in src/content/articles/, it appears at /blog/[slug] automatically
  • Sub-apps in public/ — Astro passes them straight through, zero changes needed

Build output: 3 pages in 1.16 seconds.


Phase 2: “This Looks Nothing Like My Site”

The first build looked terrible. No background, no colors, layout broken.

Three bugs, all mine (well, the AI’s — same thing at this stage):

  1. global.css was never imported in BaseLayout.astro. All custom styles, silently missing.
  2. Double Tailwind injection@astrojs/tailwind was injecting base styles and global.css had @tailwind base. Conflicting resets everywhere. Fixed with applyBaseStyles: false.
  3. A body::before z-index hack carried over from the old SCSS broke every layout. Replaced with a clean CSS gradient directly on body.

Classic migration bugs. The kind that make you go “of course” once you see them.


Phase 3: Making It Feel Like the Original

This is where I had to be the most hands-on.

The original site had a very specific vibe — dark, cinematic, with a few signature features I really didn’t want to lose:

The modal navigation. HTML5UP Dimension’s whole thing is that sections open as overlay panels. The first Astro rewrite turned it into a boring scrolling page. Called it out, rewrote index.astro from scratch: data-panel attributes on nav buttons, openPanel() / closePanel() JS, Hero always visible as background, URL hashes for deep-linking (/index.html#skills), Escape to close. Proper.

The “Sam V” smoke animation. This one’s special to me. I had custom JS (text-animator.js) and CSS (text-effects.css) that split the text into character spans and applied a rising white smoke shadow animation. Pure white rgba(255,255,255) shadows climbing to -350px offset over 10 seconds. The AI initially tried to add blue tints — no. I referenced the original code and we got it exact.

The skills matrix. #00ff41 green. Courier New. Dark terminal background. Random 25% of tags glow every 4 seconds via matrixGlow keyframes. It’s a little theatrical but I love it.

YouTube background. I have a music channel. The hero plays one of my tracks as a translucent background video. Muted on load, unmutes after 5 seconds, mute toggle in the corner. The IFrame API positioning took a few iterations across screen sizes — a lot of back-and-forth on top percentages per breakpoint.

All of this required me actively watching, testing on real devices, and saying “no, that’s wrong, look at the original.”


Phase 4: Production

A few things that matter when you’re running constrained:

NODE_OPTIONS=--max-old-space-size=400 in the Dockerfile builder stage. Without this, Node defaults to claiming up to 1.5GB of heap during astro build. Container hard limit is 512MB. OOM kill, silently. This one line prevents that.

Async font loading. Font Awesome CDN was blocking render for 1.3 seconds. Google Fonts another 833ms. Switched both to the rel="preload" onload="this.rel='stylesheet'" pattern. Near-instant FCP improvement.

YouTube deferred to first interaction. The YouTube IFrame API was loading on page load — 375KB of JS, 52 third-party cookies, before the user had done anything. Moved the whole thing behind a first-interaction listener (click/mousemove/keydown). Eliminates all of it from the initial load metrics.

Lighthouse flags I ignored: unused-css (Tailwind purges well but some base stays), YouTube thumbnail cache TTL (YouTube’s servers, not mine), and a Chrome extension showing up in the unused JS audit. Not my problem.


What This Migration Taught Me

The old way: I would have spent a weekend on this. CSS debugging, component design, wiring up the blog, fighting the build system. Probably abandoned it half-done.

The new way: I stayed in the strategic seat. I made the calls that required judgment — which framework, what to preserve, when the output was wrong, when the approach was overcomplicated. The AI handled the mechanical execution.

The thing is, the mechanical work is still hard. The CSS math for the YouTube 16:9 cover formula. The double Tailwind injection bug. The display: none fighting the JS .hidden class. These are real engineering problems. They just got solved faster because I had a collaborator who doesn’t get tired.

The human loop mattered. Every significant correction came from me testing on a real device and saying “that’s not right.” Automated tests don’t catch “the video is positioned weirdly on my phone.” Eyes on the real thing, every time.


The new site is live at k007sam.com. Clean Astro project, no legacy debt, blog works, YouTube plays in the background. Took one session.

Ship it.