Why I Rebuilt My Portfolio in Astro

March 18, 2026 (5mo ago)

I rebuilt this site in Astro, and the short version is: it ships almost no JavaScript by default, and that constraint turned out to be a feature, not a limitation.

Islands, not a single-page app

Most of a portfolio is static content. Astro renders it to HTML at build time and only hydrates the interactive bits (a theme switch, a particle effect) as isolated islands:

<ThemeSwitch client:idle />
<ProfileImage client:load />

Everything else is plain HTML and CSS. The result is a page that’s interactive the moment it paints, instead of waiting on a hydration pass for the whole tree.

Bringing my own components

The escape hatch matters as much as the default. When I genuinely needed React, for the canvas-based particle avatar, I dropped it in with @astrojs/react and a client: directive. No rewrite, no framework lock-in.

Content collections for the blog

The posts you’re reading live in a typed content collection. A small Zod schema validates the frontmatter at build time, so a typo in a date or a missing title fails the build instead of shipping broken:

const blog = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
  }),
});

Would I do it again?

Yes. The mental model is static by default, interactive on purpose. That matches how a content site actually behaves. I spend my time on content and the occasional island, not on babysitting a bundle.