Miran Arnaut logo Miran Arnaut logo
Development 6 min read

How and Why I Built This Website

From WordPress to Astro, from dynamic to static, from dependency to full control. The story behind rebuilding miran.at from the ground up.

For five years, miran.at ran on WordPress. It worked — but it never felt right.

Every update introduced new dependencies. Every plugin added JavaScript nobody asked for. The page builder fought the theme, the theme fought the caching plugin, and I fought all three.

At some point I asked myself: why am I running a full CMS backend server for a website that barely changes? Why are visitors downloading 500 KB of JavaScript just to read a blog post?

The answer was: I’m not anymore.

The Starting Point

miran.at was a classic WordPress site with a custom-built theme. It looked good, but under the hood it was a compromise.

The biggest pain point was WordPress itself. Not because WordPress is bad — it’s an excellent CMS for client projects. But for my own site, it was overengineered. I didn’t need an admin panel, a WYSIWYG editor, or media library management. I needed Markdown files, a Git repository, and a build process that outputs static HTML.

On top of that:

  • Performance: WordPress sites aren’t fast without aggressive caching. And caching often breaks the admin panel.
  • Security: Every WordPress installation is a target. Updates, hardening, monitoring — all additional work.
  • Dependency: A MySQL database server, a PHP-FPM process, regular backups. For a static website.
  • Creative freedom: WordPress forces a certain mental model — posts, pages, custom post types, taxonomies. I wanted to organize content in a way that made sense to me.

The Technology Choices

Astro — The Foundation

Astro was the right choice from the start. Its core philosophy — zero JavaScript by default, static HTML as output — matches my use case perfectly.

I evaluated other static site generators:

  • Next.js is an app framework disguised as a static site generator. Too heavy for a content site.
  • Hugo is blazing fast, but its template language feels like a step backward.
  • Eleventy is solid, but without a built-in island architecture I’d have to solve interactivity myself.
  • Astro offers the best package: type-safe Content Collections, Island Architecture, Vue/React/MDX support, and excellent developer experience.

The deciding factor was Content Collections. A Zod schema defines exactly which frontmatter fields a blog post must have. Missing a field? Wrong type? The build fails. That sounds harsh, but it prevents the most common errors in content-driven sites.

Vue 3 — Interactive Islands

Not every interaction needs a single-page application. Astro’s Island Architecture lets me use Vue components only where they’re truly needed: animated text, a cookie consent dialog, scroll-triggered animations.

Why Vue instead of React or Svelte?

  • Vue feels more natural to me. Single-File Components with <template>, <script>, and <style> in one file matches how I organize my thinking.
  • The Composition API with <script setup> is elegant and type-safe.
  • Vue’s reactivity system works behind the scenes without manual dependency optimization.

Tailwind CSS 4

I wasn’t a Tailwind fan for a long time. Utility classes in HTML felt wrong.

Then I understood: Tailwind doesn’t force you to abandon semantics. It gives you a different level of abstraction — namely zero. Instead of defining a .hero-headline class that bundles arbitrary styles in a components/hero.css file, you see exactly which styles are applied right in the HTML.

Tailwind CSS 4’s CSS-first config approach (@theme in global.css) is another step forward. No more tailwind.config.js — just real CSS custom properties.

GSAP — Motion Design

Animations on a content site need purpose. GSAP lets me control precisely what happens — and more importantly, when nothing happens.

Every GSAP animation is dynamically imported. If a visitor has prefers-reduced-motion enabled, no animation runs. Period.

ScrollTrigger powers reveal effects that accompany the reading flow without interrupting it.

Three.js — The 3D Game

A portfolio doesn’t have to be boring. The interactive 3D section (/play/) is a small experiment — a playful way to present my work.

Three.js only loads on that single page. The rest of the site stays lean.

Self-Hosting — Docker + nginx

The site runs on my own server. A Docker container with nginx serves the static files. A Cloudflared tunnel handles SSL and DDoS protection without needing to bypass Cloudflare’s proxy.

An alternative deployment path (rsync to Webmin) is ready if I ever want to switch providers.

No CDN, no external host for assets. Everything is self-hosted.

i18n — Three Languages

German, English, Croatian. Three languages, one codebase.

The implementation was trickier than expected. Astro’s i18n system supports prefixDefaultLocale: false for German as the root language. But the content routes I had to build myself — buildDefaultBlogStaticPaths() for German, buildLocalizedBlogStaticPaths() for English and Croatian.

The route manifest in route-manifest.ts defines for every route which languages are available and what URL structure they use:

  • German: /blog/wie-und-warum-ich-diese-website-gebaut-habe/
  • English: /en/blog/how-and-why-i-built-this-website/
  • Croatian: /hr/blog/kako-i-zasto-sam-izgradio-ovu-web-stranicu/

The Design

Swiss Editorial Aesthetic

This site does not look like a template. That was the single most important goal.

Inspired by Swiss design of the 1950s and 60s: clear grids, generous whitespace, typography as a design tool, no decoration for decoration’s sake.

The 8-Column Grid

Most websites use 12 columns. I use 8.

The difference is subtle but important. 8 columns forces clearer decisions. 2+6, 3+5, 4+4, 8. Fewer combinations — and that’s a good thing.

Three Colors

#08163C, #7FB5FF, #FFFFFF. Three colors. No tints, no gradients, no embellishments.

In dark mode, foreground and background swap roles. The blue remains as the accent color.

Syne — One Weight

Syne at weight 400. No bold, no semi-bold, no light.

When everything weighs the same, hierarchy must come from size and spacing. It’s more demanding — but the result is cleaner.

The Goals

1. Performance. The site loads in under a second. No JavaScript bundle blocks rendering. Lighthouse isn’t perfect — but consistently above 95.

2. Maintainability. A git pull, an npm run build, a Docker restart. Done. No WordPress updates, no plugin conflicts, no database migration scripts.

3. Complete control. Every pixel on this site is self-determined. No theme dictates how an element should look. No page builder limits me to predefined blocks.

4. Multilingual without compromise. Three languages, one codebase. Adding a translation means creating a new Markdown file with the matching translationKey.

5. Longevity. Astro produces static HTML. Static HTML will still work in 20 years. No framework upgrade needed, no runtime end-of-life, no breaking changes from React versions.

What I Learned

A redesign is never just a redesign. It’s an opportunity to question your assumptions.

Why do I need a CMS? Do I really need an admin panel? Is dynamic always better than static? How much JavaScript is truly necessary?

The answers to these questions shaped this website. And they made me a better developer.

Built with Astro 5, Vue 3, Tailwind CSS 4, GSAP, and Three.js. Self-hosted on my own infrastructure. In three languages.

This is what my website looks like now. This is what it will look like for years to come.

(05) Get in touch

Let's work together

Send me a message or connect on social media.