[BACK]
Computer screen showing code

How I rebuilt my personal site with Astro

· Updated

My website started its life in Flutter. It was simple enough: home, about, and a contact page. Nothing wild. I always meant to add a blog, but I kept putting it off because, well, Flutter on the web works, but it feels like you’re fighting the medium.

It’s fantastic for apps, don’t get me wrong. But for a content site? You’re dealing with packages that break on web, weird dart:io issues, text rendering quirks, and SEO that feels like a hackathon project. For a simple portfolio, the friction just wasn’t worth it. I pulled the plug and decided to rebuild with actual web tech.

The React Trap

Naturally, I looked at React first. It’s practically synonymous with “modern web development” at this point. Every job post asks for it; every tutorial defaults to it. Even though my background was more Vue and old-school PHP/Laravel, I figured React was the “correct” choice in 2025.

Turns out, “popular” doesn’t mean “simple.”

React forces a specific mental model on you. You’re constantly juggling renders, dependency arrays, side effects, and state, even for static content. I found myself over-engineering a solution for “display text on screen.” The conceptual overhead felt surprisingly heavy for what should have been a lightweight project.

And then there’s the ecosystem fatigue. Before writing a line of code, I was vetting bundlers, routers, state management libraries, and linting rules. I spent days reading “Best Practices for 2025” articles instead of actually building the site. It felt… noisy.

Stumbling onto Astro

Frustrated, I started digging for alternatives and caught a mention of Astro in a random Reddit thread. The pitch was refreshing: “just write HTML and CSS, ship zero JS by default.”

It sounded like the way web development used to feel, before we started sending 2MB of JavaScript to clear a div. I was skeptical, but curious.

A weekend later? I had the entire site ported, plus the blog section I’d been procrastinating on for months. It just clicked.

The design: a love letter to vim and TUIs

Full disclosure, I don’t actually use Vim. I write all of this in VS Code like a perfectly normal person. That said, I do spend a lot of my day in the terminal using git, tmux, Claude Code, a bunch of other CLI stuff, and something about vim and TUIs has always felt cool and hacky and delightfully computer-y to me, and I wanted that energy on my website.

So the whole thing is dressed up like a vim buffer:

  • Monospace everywhere (JetBrains Mono).
  • A near-black background with muted greys for body text and brighter foregrounds for links, just like a dim colorscheme.
  • A sticky status bar at the bottom of every page with the current path (~/blog), a -- NORMAL -- / -- VISUAL -- indicator that flips when you select text, and a Top / Bot / percentage scroll position. It’s purely cosmetic, but it makes me smile.
  • Navigation links written as Ex commands (:about, :blog, :contact, :work), because that’s how I’d actually open them in my head.

It’s a weird flex for a personal site, but the constraint was useful. Once I committed to “looks like a vim buffer,” a lot of design decisions got easier. No marketing hero, no gradients, no glassmorphism, just text on a dark background.

How it’s set up

The structure is super clean. It doesn’t feel like an “app”; it feels like a website.

src/
├─ components/          # BackButton, Newsletter, SEO, StatusBar
├─ content/
│   └─ blog/            # markdown/MDX posts, one folder per post
├─ content.config.ts    # Zod-typed Content Collections schema
├─ layouts/
│   └─ BaseLayout.astro # the master template
├─ pages/
│   ├─ index.astro      # homepage
│   ├─ about.astro
│   ├─ contact.astro
│   ├─ blog/            # listing + dynamic [...id].astro route
│   ├─ work/
│   ├─ api/             # newsletter subscribe + verify endpoints
│   ├─ rss.xml.ts       # full-content RSS feed
│   ├─ robots.txt.ts
│   └─ llms.txt.ts
├─ styles/
│   └─ global.css       # Tailwind v4 + a few custom tokens
└─ utils/
    └─ seo.ts           # JSON-LD + canonical URL helpers

The killer feature is how it handles content. Blog posts are literally just Markdown (or MDX) files inside a Content Collection. No CMS to configure, no database, no complex JSX-to-HTML pipelines.

---
title: "My Post"
pubDate: "2025-09-19"
description: "Just a regular blog post"
tags: ["web", "astro"]
---

# This is just markdown

And it works exactly like you'd expect.

That YAML block at the top (frontmatter) handles all the metadata. content.config.ts runs it through a Zod schema at build time, so I can’t accidentally publish a post without a date or with a misspelled field. The build just refuses.

Here’s a quick breakdown of the moving parts:

  • Components: small, self-contained pieces. StatusBar is the vim-mode footer, Newsletter is the email signup, SEO handles meta tags and Open Graph, BackButton is a one-liner I reuse on subpages.
  • Layouts: BaseLayout.astro does the heavy lifting, the HTML skeleton, meta tags via the SEO component, the status bar, JSON-LD, the lot. Every page just wraps itself in this.
  • Pages: the file system is the router. index.astro is home. about.astro is /about. The blog/[...id].astro file uses getStaticPaths over the blog collection to generate one route per post.
  • Content: posts live under src/content/blog, each in its own folder so images can sit right next to index.mdx and get optimized by Astro’s image pipeline.
  • Styles: Tailwind v4 driven from a single global.css, with a small set of custom design tokens (the vim-bg, vim-fg, vim-link colors) defined via @theme. No SCSS, no PostCSS config, no preprocessor babysitting.
  • Utils: small TypeScript helpers. seo.ts builds canonical URLs and the shared Person/Website JSON-LD I drop into every page.

A few extras worth mentioning: the site is deployed to Cloudflare via @astrojs/cloudflare and wrangler, the RSS feed is full-content (so you don’t have to leave your reader), there’s a sitemap from @astrojs/sitemap, and the newsletter is a tiny Resend-backed flow with a double opt-in, all running on the same Cloudflare Worker.

Why it stuck

Astro just fit the problem I was solving. I wanted a personal site, not a Single Page Application. I didn’t need client-side routing or complex state management; I needed HTML that loads fast.

The Content Collections feature is a godsend for blogs. I get type safety on my frontmatter (so I can’t accidentally publish a post without a date), and it handles all the build-time generation.

It feels like making a website in the good old days. You’re writing actual HTML, CSS, and JS, but with the power of modern tooling. I stopped fighting the tools and started writing content again. Development is fast, simple, and honest.

Final thoughts

I went into this assume React was the default answer. I was wrong.

React isn’t “bad,” obviously. If I were building a dashboard or a complex interactive app, I’d pick it up in a heartbeat. But for a blog? A portfolio? A documentation site? It’s overkill.

  • Big, state-heavy app? React/Vue/Svelte.
  • Content-focused site? Astro.
  • Flutter web? Look, I love Flutter, but… maybe let’s keep it on mobile for now.

You can check out the source on GitHub.

---

Subscribe to get an email when I publish a new post. No spam. I promise! Or use RSS.