Rebuilding My Portfolio with Astro: Why I Left Next.js
· English · Ghostwritten by Claude Sonnet 5
My old portfolio was a Next.js app. It worked fine, but the entire site — a static bio, a certifications list, a couple of project showcases — was shipping React’s hydration runtime to every visitor, on every page, whether or not the page actually needed any client-side interactivity. Almost none of it did.
Zero JS by default
Astro’s model flips the default: components render to plain HTML unless you explicitly opt a
piece of the page into client-side JavaScript with a client: directive. The only part of this
site that actually needs to run in the browser is the AI chat widget, so that’s the only part
that ships a JavaScript bundle:
---
import { ChatWidget } from '../components/ai/ChatWidget';
---
<ChatWidget client:idle />
client:idle hydrates once the browser is idle, so it never competes with the initial render for
resources. Everything else on the page — the bio, the certification cards, the footer nav — is
static HTML, generated at build time, with no runtime cost at all.
Content Collections instead of hand-rolled data files
Certifications, project showcases, and now blog posts (this one included) are all typed content collections, validated against a Zod schema at build time:
const blogSchema = z.object({
title: z.string().min(1),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
summary: z.string().min(1),
});
If I forget a required field in a post’s frontmatter, the build fails immediately with a clear
error — not a runtime undefined bug three pages deep.
Server logic without a separate API layer
The one genuinely dynamic feature — the AI chatbot — needed a real backend: rate limiting, encryption, a call out to Anthropic’s API. In the old Next.js app that meant a Server Action. In Astro, Actions fill the same role: typed RPC functions that run only on the server, callable from the browser without hand-writing a REST endpoint or exposing any secret to client code.
The migration wasn’t just a framework swap — it was also a chance to fix things I’d wanted to fix for a while (a rate limiter that actually works across multiple instances, a hybrid-encryption implementation that doesn’t depend on a third-party crypto library). I’ve written those up separately.