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.