I had a blog. It ran on Next.js with Hashnode as the headless CMS. On paper that sounds reasonable — a proper framework, a dedicated CMS, separation of concerns. In practice it was a slow, fragile thing that I didn’t enjoy writing on.

The fetching would fail silently. ISR would revalidate at the wrong time. The Hashnode dashboard felt like an extra job. And somewhere in the middle of debugging a content fetch at 11pm I thought — why am I doing this to myself? It’s a personal blog. I write about F1 and software. Nobody is buying anything here.

So I rebuilt it. With Astro. And it took less time than I expected.

What Was Actually Wrong

The core problem with the old setup wasn’t Next.js — Next.js is a great framework. The problem was using a network-dependent CMS for content that never needed to be dynamic.

Every page load was making an API call to Hashnode. That API call could fail. It could be slow. It added latency to every single page even though the content hadn’t changed in days. I was paying a runtime cost for zero runtime benefit.

The second problem was that I built the whole thing too fast without fully understanding what I was doing. Rapid development is fine when you know what you’re building. When you don’t, you end up with a tangle of useEffect hooks and useState calls that you’re afraid to touch.

// what my data fetching looked like
useEffect(() => {
  fetch('/api/posts')
    .then(res => res.json())
    .then(data => setPosts(data))
    .catch(err => console.error(err)); // and then what?
}, []);

That catch block is doing nothing useful. That’s a real production bug sitting there, silently swallowing errors.

Why Astro

Astro’s pitch is simple — ship zero JavaScript by default. For a blog that is entirely static content, that’s not a tradeoff. That’s just correct.

Every other framework ships a runtime to the browser. React, Next.js, SvelteKit — they all send JavaScript that has to parse and execute before your user sees anything. For a blog post, nothing needs to be interactive. The user reads. They scroll. They leave. There is no state to manage.

Astro builds your pages at deploy time and ships pure HTML. Fast by default, not by optimisation.

The other thing that sold me was Content Collections. You define a schema for your posts in TypeScript, and Astro validates every MDX file against it at build time. If you forget a required field, the build fails loudly. That’s the kind of strictness that saves you from yourself.

// src/content/config.ts
const blog = defineCollection({
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      date: z.coerce.date(),
      category: z.enum(['dev', 'f1', 'tech']),
      tags: z.array(z.string()),
      excerpt: z.string(),
      draft: z.boolean().default(false),
      coverImage: image().optional(),
    }),
});

If I push a post without a title, it doesn’t go live with a broken page. It fails at build time. That’s what I want.

The Writing Workflow

This was the part I was most unsure about. The Hashnode dashboard had one thing going for it — it was a proper writing interface. Markdown preview, image uploads, a dedicated space.

I replaced it with Obsidian.

Obsidian is a local markdown editor. Free for personal use, no subscription, no cloud dependency. You open your vault — which is just a folder — and write. Drag an image into the note and it saves next to your MDX file automatically. The writing experience is genuinely better than any web-based CMS I’ve used.

The publishing workflow is:

1. Write the post in Obsidian
2. Set `draft: false` in the frontmatter when ready
3. `git push`
4. Vercel builds and deploys in under 30 seconds

No dashboard login. No API keys. No CMS to go down at the wrong moment. The content lives in the repo, versioned alongside the code.

What the Build Actually Looks Like

The folder structure is flat and obvious:

src/
  content/
    blog/
      my-post.mdx
      images/
        my-post-cover.jpg
  pages/
    index.astro
    all.astro
    blog/
      [slug].astro
  components/
    Nav.astro
    PostCard.astro
    WideCard.astro
  layouts/
    BaseLayout.astro

Each page is an .astro file. Components are .astro files. There’s no client-side routing, no global state, no provider tree. It feels like building for the web again.

The Performance Difference

The difference is obvious just loading the pages. The old blog had a perceptible delay as content fetched from Hashnode. The new one is instant.

Static HTML served from a CDN edge node is as fast as a website can be. There’s nothing to optimise because there’s nothing running.

One Thing Astro Gets Right That Nobody Talks About

Islands architecture.

Astro lets you mix static and interactive content at the component level. If you want a static blog post with one interactive element — a live chart, a code playground — you make just that component interactive with a client: directive. Everything else stays static.

<!-- this component ships JS and hydrates in the browser -->
<InteractiveChart client:visible />

<!-- this one ships zero JS -->
<PostCard title={post.data.title} />

I don’t need this for a blog right now. But when I eventually want to add a live F1 telemetry widget or an interactive data visualisation, I won’t need to rebuild anything. It’s already there.

Would I Recommend It

For a personal blog, content site, portfolio, or documentation — yes, without hesitation.

For a complex application with authentication, real-time data, or heavy user interaction — probably not. Next.js is a better fit there.

But for writing on the internet? Astro is the right tool. The setup is simple, the output is fast, the writing workflow gets out of your way. It does exactly what a blog needs and nothing it doesn’t.

That’s rare enough to be worth writing about.