Skip to content
Rezha Julio
Go back

Why I Left Zola's Simplicity for Astro's Power

4 min read

A little over a year ago, I wrote about migrating from Hugo to Zola. My reasoning back then was solid: I was tired of Go Templates’ idiosyncrasies, and Zola’s Tera engine felt like home (hello, Django background!). Plus, Zola is written in Rust, builds fast, and ships as a single binary. What’s not to love?

I thought Zola was the endgame. Simple, fast, opinionated.

But here I am, writing this post on a blog built with Astro. Yes, I traded a Rust binary for a node_modules folder the size of a small black hole.

Why? It wasn’t about speed. It was about ceiling.

The “Batteries Included” Trap

Zola is fantastic because it comes with everything you need built-in: syntax highlighting, search, image processing, Sass support. You don’t need plugins. In fact, you can’t really have plugins.

That’s the trade-off. When Zola does exactly what you want, it’s bliss. But the moment you want to step slightly outside its boundaries, you hit a wall.

For example, I wanted to add a clap button to my posts. In Zola, this meant manually wiring up client-side JavaScript, fighting with template variables to pass data to scripts, and ensuring it didn’t break on different page layouts. It felt like I was fighting the tool.

Enter Astro: The “Component” Model

I resisted Astro for a long time. “It’s JavaScript,” I told myself. “I don’t want a complex hydration process for a static blog.”

But Astro’s Islands Architecture changed my mind. It sends zero JavaScript to the client by default. It’s just HTML generation, but with a DX that makes traditional template engines feel painful.

Coming from a Django background, I used to love template inheritance ({% block content %}). But after using Astro’s component-based model, I realized how much cleaner it is to compose a UI from small, reusable parts.

Instead of a monolithic base.html with fifty {% if %} blocks, I have a <Layout> component wrapping a <Header>, <Main>, and <Footer>. If I want a specific interactive widget on just one page? I import it and use it.

---
import ClapButton from '../components/ClapButton.astro';
---
<Layout>
<article>
{/* Content */}
</article>
<ClapButton client:visible />
</Layout>

That client:visible directive is magic. It tells Astro: “Render this as static HTML, but hydrate it with JavaScript only when the user scrolls it into view.”

The Ecosystem Difference

Zola lives in a small corner of the Rust ecosystem. Astro sits on top of npm.

Adding Tailwind CSS to Zola meant configuring a separate build step or relying on Zola’s Sass processing. In Astro? npx astro add tailwind. Done.

Generating dynamic Open Graph images for every post? In Zola, I’d have to write an external script to run at build time. In Astro, I wrote a helper function using satori and it just works.

Yes, dealing with dependencies is annoying. But having access to the entire npm ecosystem means I stop reinventing things.

Is It Slower?

Technically, yes. Zola builds in milliseconds. Astro takes a few seconds because it has to spin up Node.js.

Does it matter? For a blog with a few hundred posts, the difference is negligible in CI/CD. The time I save developing features in Astro far outweighs the few extra seconds I wait for a deploy.

So Why Bother?

Zola is still great if you want a simple, fast, zero-dependency static site. I’d still pick it for documentation sites or quick landing pages.

But for a personal blog where I keep wanting to bolt on weird interactive things, Astro gets out of my way. I get static HTML by default and JavaScript only where I ask for it.

Is this the final migration? Of course not. But for now, I’m happy here.


Related Posts


Next Post
The Art of Yak Shaving