Today I sat down with Claude Code and rebuilt parts of my personal site from scratch. Not a planned sprint — just a conversation that turned into a full day of shipping. Here's exactly what we did, in order.
The Stack
The site runs on:
- Astro 6 — static site generator with hybrid rendering
- Tailwind CSS — utility-first styling with JetBrains Mono and a custom
#6ee7b7accent - Cloudflare Pages — edge deployment with Wrangler for config
- Keystatic — git-based CMS for this very blog
Every piece of this was chosen and wired up with Claude Code in an interactive session.
What We Did
Config Cleanup
The first thing on the list was converting wrangler.toml to wrangler.jsonc and enabling Cloudflare observability. Before touching anything, Claude fetched the pricing page and confirmed: 200,000 log events per day on the free plan, dropping to 1% sampling if you go over — your Worker keeps running, you just lose visibility.
We also added .nvmrc with Node 24, and sorted certificates descending by issuer group and by date within each group — all in static source data, no runtime logic.
Adding the Blog
The main event. The goal was a /blog route with individual post pages, powered by a CMS. We chose Keystatic — git-based, fully free, stores content right in the repo.
The plan sounded simple. The execution was not.
What we installed:
@keystatic/core+@keystatic/astro— the CMS@astrojs/cloudflare— for production deployment@astrojs/markdoc— for.mdoccontent files@astrojs/react— Keystatic's admin UI requires React@tailwindcss/typography— for blog post prose styling
Each integration was added via npx astro add per the project's custom /astro skill.
The Bugs
1. Astro 6 removed output: 'hybrid' — merged into output: 'static'. Easy fix.
2. tslib wouldn't install — graphql-tag (a Keystatic transitive dependency) imports tslib, but npm install silently skipped it. The fix was npm ci, which does a strict clean install from the lockfile.
3. module is not defined in the Cloudflare adapter — @astrojs/cloudflare v13 runs dev through a Cloudflare Workers simulation. Keystatic's CJS dependencies break in that environment. Fix: only apply the adapter when CF_PAGES=1.
4. Keystatic should not go live in production — with storage: { kind: 'local' }, the admin would be a broken, publicly accessible UI in production. Fix: only include the Keystatic integration when NODE_ENV !== 'production'.
5. require() in an .mjs file — @tailwindcss/typography was added with CommonJS require() in an ES module config. The whole site lost its styles silently. Fixed with a proper import.
6. defineConfig doesn't accept a function — Astro's defineConfig expects a plain object, not a function like Vite's. The entire config became a no-op. Fixed by reading process.env directly.
Later: Shipping to Production
Once the blog was working locally, pushing to Cloudflare Pages immediately broke.
7. .wrangler/deploy/config.json was committed to git — this file gets written by the @astrojs/cloudflare adapter during a local build. It points to dist/server/wrangler.json, which only exists when there's a server entry point. In static mode, that file is never generated, so npx wrangler deploy in CI failed with a missing config error. Fix: git rm -r --cached .wrangler/ and add .wrangler/ to .gitignore.
Connecting the Blog to the Home Page
With the blog live, the next step was surfacing it on the portfolio itself. Added a #blog section to the home page that fetches the three most recent posts at build time — date, title, excerpt — with a "View all posts →" link at the bottom. The nav link was updated from /blog to #blog so it scrolls inline.
Cleaning Up the Nav
The nav had been copy-pasted across three files: home page, blog listing, and individual posts. The home version had a full hamburger menu with section links; the blog pages had a minimal logo-and-back-link version.
The fix was a Navbar.astro component with a variant prop — "home" for the full menu, "back" for the simple header. Then, rather than importing the component in every page, it was moved into Layout.astro so the nav renders automatically. Pages just pass navVariant, backLabel, and backHref as layout props — no component imports, no duplicated markup.
What It Actually Felt Like
The flow was closer to pair programming than prompting. I'd describe what I wanted, Claude would propose an approach, and I'd either approve it, redirect it, or reject specific steps — like when I asked it to switch to JSONC format mid-task, or when it was about to copy a package from a sibling project and I asked if that would survive a production build (it wouldn't).
The biggest adjustment was learning when to let Claude run and when to step in. It moves fast — sometimes too fast — so staying in the loop on decisions that matter kept things from going sideways.
Seven bugs plus a deploy failure. None of them were hard in isolation — but stacked together, without a tool to move through them quickly, it would have been a much longer day.
Final Thoughts
Claude Code isn't magic. But it's fast, it remembers context, and when something breaks it investigates rather than guesses. For solo developers, that matters.
The productivity gains are real, but you still need to understand what's being built. Claude is a great co-pilot. It's not autopilot.