Engineering #astro #performance #web #portfolio

Why I Chose Astro for My Portfolio

After trying Next.js, Remix, and plain Vite — I landed on Astro. Here's why the island architecture and content collections made it the right fit.

February 13, 2026 4 min read

The Portfolio Problem

A portfolio is a strange beast. It’s mostly static — hero sections, project cards, a blog — but you also want React islands for animations, interactive demos, and components. Most frameworks optimize for one or the other. You either get a full SPA with heavy JS bundles, or a static site generator that fights you every time you want client-side interactivity.

Astro solves this elegantly.

The Island Architecture

In Astro, components are server-rendered HTML by default. Zero JavaScript shipped to the browser unless you explicitly opt in. To hydrate a component on the client, you use a directive:

<TextRandomized client:load />
<HeroScroll client:visible />
<InteractiveDemo client:idle />
  • client:load — Hydrates immediately on page load
  • client:visible — Hydrates when the element enters the viewport
  • client:idle — Hydrates when the browser is idle

For a portfolio, this is perfect. The hero animation loads immediately. The sections lower in the page wait until they’re visible. Background demos wait for idle time. You get fast paint, then progressive enhancement.

Content Collections

Content Collections are Astro’s type-safe layer over your markdown/MDX files. You define a schema:

import { defineCollection, z } from "astro:content";

const blog = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    category: z.string(),
    readTime: z.number(),
  }),
});

Then query it in your pages:

const posts = await getCollection("blog");
const sorted = posts.sort(
  (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
);

The schema runs at build time. If a markdown file is missing a required field, the build fails with a clear error. No silent undefined bugs reaching production.

File-Based Routing

Every file in src/pages/ becomes a route. A [slug].astro file with getStaticPaths generates every blog post URL at build time:

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

It just works. No router configuration, no special setup.

The Trade-offs

Astro isn’t perfect for everything. If you need heavy real-time interactivity — a dashboard, a collaborative editor — the island model becomes awkward. You’ll end up hoisting so much state up to React that you’ve effectively reimplemented a SPA inside Astro.

But for a portfolio? It’s exactly the right tool. Static by default, interactive where it matters, fast everywhere.

Build Output

The final build output is a folder of HTML files, CSS, and minimal JS chunks. Deploy to any CDN — Netlify, Vercel, Cloudflare Pages — with zero configuration. No server required, no runtime to maintain.

For a portfolio that needs to be fast, cheap to host, and easy to maintain: Astro is hard to beat.