|

Why I'd Pick Astro for My Next Website (Part 1/7)

So here’s the thing — Hugo has been running this site for a while now, and honestly? It does the job. Fast builds, minimal fuss. But I keep catching myself fighting with Go templates and wishing I had the kind of flexibility I’m used to from my day-to-day TypeScript work.

I’ve been looking into Astro, and I think it might be exactly what I need for a fresh start. Let me walk you through what’s caught my eye and how I’d plan a migration from Hugo.

What’s making me itch for something new?

Don’t get me wrong — Hugo is solid. But a few things keep bugging me:

Go templates are… an experience. Have you ever nested {{ with .Params.hero }}{{ if .enable }} six levels deep? It’s not pretty. Every time I want to change something in a layout, I spend more time decoding template logic than actually building.

Partials aren’t components. Hugo has partials, sure, but they don’t feel like real components. No typed props, no clean composition. I end up copy-pasting HTML more often than I’d like to admit.

No TypeScript. I write TypeScript all day at work. Coming back to a site with no type safety feels like driving without a dashboard.

So why Astro?

Three things sold me on it:

Zero JavaScript by default. Astro renders everything to HTML at build time. Your page loads with zero framework overhead. But if you need something interactive — a contact form, a theme toggle — you can hydrate just that one component:

---
const title = "Hello from the server"
---

<h1>{title}</h1>

<!-- only this little guy ships JS -->
<ContactForm client:load />

That’s pretty cool. Static where it can be, dynamic only where it needs to be.

File-based routing. Drop a file in src/pages/, boom, it’s a route:

src/pages/
├── index.astro          → /
├── about.astro          → /about
└── blog/
    ├── index.astro      → /blog
    └── [slug].astro     → /blog/:slug

No config file to maintain. Love it.

Content Collections with Zod. Astro validates your content with schemas. Missing a title on a blog post? Build error, not a broken page in production:

// src/content.config.ts
import { defineCollection, z } from 'astro:content'

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    date: z.date(),
    tags: z.array(z.string()),
    draft: z.boolean().default(false)
  })
})

export const collections = { blog }

Type safety for content. Yes please.

What about build speed?

Hugo builds in milliseconds. Astro won’t match that — but realistically, a site this size would build in maybe 3 seconds. For something I deploy a few times a week, I genuinely don’t care about the difference.

Where Astro would actually win is the output. No framework runtime, no hydration scripts, just HTML and CSS. The kind of PageSpeed scores you can brag about.

What a Hugo migration would look like

Here’s how my current Hugo stuff would map to Astro:

HugoAstro
config.toml paramsConfig + component props
layouts/ templatessrc/layouts/ + src/components/
content/en/blog/*.mdContentful entries
static/ assetspublic/
themes/resume/Custom components
Go template partialsAstro components with typed props
languages.en/de configLocale-prefixed routes
docker/DockerfileUpdated Dockerfile (Node instead of Hugo)

The biggest piece is the blog content — 20+ markdown posts. Those would move to Contentful instead of staying as files. More on that in Part 2.

The Hugo theme would go entirely. I’d build components from scratch instead. More upfront work, but no more wrestling with someone else’s template decisions.

Setting up the project

Getting started is dead simple:

npm create astro@latest ./

Pick “Empty”, build from scratch. The structure you get:

├── astro.config.mjs
├── package.json
├── public/
│   └── favicon.svg
├── src/
│   ├── layouts/
│   │   └── Base.astro
│   ├── components/
│   ├── pages/
│   │   └── index.astro
│   └── styles/
│       └── global.css
└── tsconfig.json

And the config:

// astro.config.mjs
import { defineConfig } from 'astro/config'

export default defineConfig({
  output: 'static',
  site: 'https://oltionzefi.com'
})

output: 'static' is actually the default, but I like spelling things out.

The mental shift from Hugo

If you’re coming from Hugo, here’s the thing to get your head around: Hugo thinks in templates and content types. Astro thinks in components and data.

In Hugo, you have a template that receives a page context. In Astro, the component itself fetches data and renders HTML — all in one file:

---
import Layout from '../layouts/Base.astro'
import { getCollection } from 'astro:content'

const posts = await getCollection('blog')
const sorted = posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
---

<Layout title="Blog">
  <ul>
    {sorted.map(post => (
      <li>
        <a href={`/blog/${post.slug}`}>{post.data.title}</a>
        <time>{post.data.date.toLocaleDateString()}</time>
      </li>
    ))}
  </ul>
</Layout>

No separate template files, no data cascade to debug. Just TypeScript above the --- fence and markup below it.

What’s coming next

This is a 7-part series. Here’s the roadmap:

  1. Why Astro (this one)
  2. Setting up Contentful — content models, API setup, migration from markdown
  3. Connecting Astro to Contentful — data fetching, dynamic routes, Rich Text rendering
  4. Static build optimization — images, SEO, sitemap, RSS
  5. Docker + Nginx deployment — multi-stage build, caching, CI/CD
  6. Type-safe content with Zod — validating Contentful data at build time
  7. Securing secrets — Docker BuildKit secrets, keeping tokens out of image layers

If you’re thinking about a similar move, stick around. I’ll share every decision, every gotcha, and every “oh that’s nice” moment along the way.

Keep pushing forward and savor every step of your coding journey.