← All posts

Building This Portfolio with Astro v6

Why Astro?

For a portfolio site with no real-time data requirements, Next.js is overkill. Server components, app router, edge functions — none of that matters when you’re shipping static HTML to Cloudflare Pages.

Astro v6 gives you:

The result is a site that’s fast by default, not by optimization.

Content Collections Architecture

Every section on this page lives in src/content/*.md. The schema is defined once in src/content.config.ts using a passthrough Zod schema — each markdown file can define whatever frontmatter shape its component needs.

const sections = defineCollection({
  loader: glob({ pattern: '*.md', base: './src/content' }),
  schema: z.object({ /* common fields */ }).passthrough(),
});

The page loads all sections at build time with getCollection('sections'), builds a keyed map by ID, and passes each section’s data as props to its component.

Benefits:

Tailwind CSS v4

Tailwind v4 drops the JavaScript config file entirely. Configuration lives in CSS:

@import 'tailwindcss';

@theme {
  --font-sans: 'Inter', ui-sans-serif, system-ui;
  --animate-orb-drift: orb-drift 14s ease-in-out infinite;
}

The Vite plugin picks this up and generates utilities on demand. No tailwind.config.mjs, no content glob, no purging.

The Theme System

The light/dark theme is pure CSS custom properties. Every component uses var(--bg-card), var(--text-primary), etc. — never hardcoded Tailwind color classes.

:root {
  --bg-card: rgba(255, 255, 255, 0.8);
  --text-primary: #0f172a;
}

[data-theme="dark"] {
  --bg-card: rgba(255, 255, 255, 0.05);
  --text-primary: #ffffff;
}

The toggle writes to localStorage and updates data-theme on <html>. An inline script in <head> reads the saved preference before first paint, eliminating flash of unstyled content.

Result

Static HTML. No client-side JS except the theme toggle and scroll animations. Sub-second LCP. Deployed to Cloudflare’s global edge network in seconds.

Astro was the right call.