Budding

Migrating this site from Next.js/Vercel to Astro/Cloudflare, with AI driving the work

A dormant Next.js blog became a static Astro site on Cloudflare Pages — foundation, portfolio content, live AI features, and a production domain cutover, all built through an agentic plan-execute-verify loop.

This site used to be a dormant Next.js 14 blog on Vercel — five posts, all from 2024, no content in almost two years. This note is the short version of how it became what you’re reading now: a static Astro site on Cloudflare Pages, with a digital garden, abstract case studies, and a couple of real, guardrailed AI features. The point of writing it down isn’t just changelog-keeping — the how is itself the proof of the “AI engineer” claim this site makes. Not “I used ChatGPT to write some code,” but a repeatable, verified workflow.

The shape of the work

Three phases, roughly:

  1. Foundation. Replace the Next.js app wholesale with Astro — content collections for notes and case studies, wiki-links and backlinks for the garden, client-side Mermaid (no more headless-Chromium build step), search, RSS, the legacy /blog/article/<slug> URLs preserved for SEO.
  2. Interactive + AI features. A schema-driven form builder island, a prompt-to-form generator backed by Claude, and a retrieval-augmented “ask about my work” chat over the site’s own content — Vectorize for the vector store, Workers AI for embeddings, Claude for the streamed answer.
  3. Infrastructure migration. Provision the real Cloudflare side (Pages project, Vectorize index, CI/CD), cut the custom domain over from Vercel, and retire the old host.

Each phase used the same loop: plan, written down and reviewed before any code changed; execute in small steps, each one checked; verify against the real system, not just against the plan. The plan for the whole rebuild lives in this repo as docs/revamp-plan.md, with a running status log — an agent (or a future me) can read it cold and know exactly where things stand.

Where “verify, don’t assume” actually paid off

It’s easy to claim an AI workflow catches its own mistakes. Here’s where it concretely did, on this project:

  • A silent slugification bug. Astro’s content loader strips dots from file-based ids. A note about “Next.js” would have quietly resolved to the wrong URL if the build output hadn’t been checked directly instead of trusted on faith.
  • A 64-byte limit, found on the first live run. Cloudflare Vectorize caps vector ids at 64 bytes. Offline tests didn’t catch it because the test fixtures didn’t happen to use a long, dated slug — but a real note did, and the first live upsert failed with id too long. Fixed with a short stable hash fallback, plus a regression test so it can’t silently regress.
  • Citations linking to the wrong path. A URL-building helper returned a bare frontmatter slug instead of the prefixed /blog/article/<slug> path — every legacy-note citation from the RAG chat was broken. An existing unit test had actually masked this, because its fixture happened to pass in an already-prefixed value. Caught by checking real retrieval output, fixed, and the test corrected so it exercises the real code path.
  • A production domain silently 404ing, mid-migration. More on this below.

None of these were exotic — they’re the ordinary kind of bug that shows up the moment code meets a real system. The workflow’s job wasn’t to prevent them; it was to make sure they got caught before anyone else saw them, by checking real output at every step instead of assuming code that looks right behaves right.

The domain cutover, and the outage in the middle of it

The infrastructure migration is the freshest example, so it’s worth telling in full. DNS for this domain was already on Cloudflare. Ahead of finishing the Cloudflare side, the domain was pulled from the Vercel project and added as a custom domain on the new Cloudflare Pages project — reasonable moves, taken slightly out of order.

Checking the live state before writing an execution plan (rather than trusting the docs about what “should” be true) turned up a real, live problem: the apex domain was 404ing — Cloudflare had the custom domain attached but no production deployment to serve, only a preview build from days earlier. www was worse — still pointed at Vercel, which now rejected it with a “deployment not found” error since the domain had been removed there. Both hostnames were broken for anyone visiting right then.

The fix was small and fast once identified: deploy the already-tested build directly as production (wrangler pages deploy --branch=main, which is what Cloudflare Pages actually checks to decide “is this the live one”), and repoint the stale www CNAME from Vercel to the new project. The last piece — a canonicalizing redirect between the two hostnames — took one more turn: scripting it hit a token permission gap, so the domain’s owner configured it directly instead, choosing www as canonical (apex now 301s to www — the reverse of the working assumption baked into astro.config.mjs’s site field, a one-line fix once flagged). The instructive part isn’t the fix — it’s that checking the live system before acting, twice over, turned two assumed-fine steps (the migration order, and which hostname was canonical) into caught-and-corrected details instead of missed ones.

What’s deliberately not live yet

The AI features (the prompt-to-form generator and the RAG chat) are fully built, unit-tested, and even provisioned end-to-end on Cloudflare — the vector index exists, content is embedded, retrieval returns correctly cited answers. They’re just not switched on for visitors yet. That’s a sequencing choice, not a technical gap: infrastructure and CI/CD first, AI features as their own deliberate launch. The /ai page’s build log has the detail on each feature; this note is about the migration around them.

Where this leaves things

Static site, live custom domain, CI/CD wired through GitHub Actions so a push to main runs tests, a check-gated build, a confidentiality gate, and a deploy — no more manual wrangler invocations for routine changes. The old Vercel project and its now-orphaned custom domain are next in line to be retired properly.

This note will likely get folded into something more permanent once the AI features actually launch — for now it’s the honest, current account of how a two-year-dormant blog became this.