All writing

Building My Own Static Site Generator: A 9-Year Journey Home

After nine years hopping from Jekyll to Hugo to Nuxt to Next.js, a Kotlin developer with just enough JavaScript builds his own static site generator.

The Wandering Years

Nine years ago, I made a decision that would haunt me for nearly a decade: I left Jekyll.

Four different stepping stones on a winding path that ends at home

Don't get me wrong—Jekyll served me well. It was simple, it worked, and GitHub Pages made deployment trivial. But I wanted more. I wanted speed. I wanted modern tooling. I wanted that shiny new thing everyone was talking about. (By day I write Kotlin—these days Kotlin Multiplatform apps at the studio—so the web was always the side quest.)

So I moved to Hugo. Then Nuxt. Then Next.js. Each time, I told myself this was the one. Each time, I found myself fighting the framework more than writing content. Configuration files grew. Build times increased. Dependencies multiplied. And somewhere along the way, I lost sight of what I actually needed: a simple way to publish my thoughts.

The Spark

One day, while procrastinating on actual work, I stumbled upon jakewharton.com. Jake Wharton—the legend of Android development—had a website that was... simple. Brutally, beautifully simple. No fancy animations. No JavaScript frameworks. Just content, presented cleanly.

I loved it.

Digging deeper, I found that the syntax highlighting CSS traced back to mojombo/tpw—Tom Preston-Werner's personal website. The co-founder of GitHub. Built with Jekyll.

The irony wasn't lost on me. After years of framework-hopping, the answer had been there all along: simplicity.

But I didn't want to go back to Jekyll. I wanted something that was mine. Something I understood completely. Something I could mold to my exact needs.

The Challenge

Here's the thing: I'm a Kotlin developer. Android is my world. JVM is my comfort zone. JavaScript? I know just enough to be dangerous—and by dangerous, I mean dangerous to myself.

But I knew exactly what I wanted:

  • Markdown with frontmatter — because writing should feel like writing
  • Syntax highlighting — with my beloved Android Studio theme
  • Series support — for multi-part tutorials
  • OG image generation — because social previews matter
  • PWA with offline support — because why not?
  • A beautiful development experience — not just functional, but enjoyable

Could I build this? With my limited JavaScript knowledge?

I decided to try. And I had a secret weapon: Claude.

The Journey

What started as a simple build script evolved into something I'm genuinely proud of. Let me walk you through the key phases.

The Foundation

The first step was humble: a basic static site generator that could parse markdown and generate HTML. Nothing fancy. But it worked.

Within the first few iterations, I had:

  • A modular build system
  • Production minification (14% size reduction!)
  • A content manager CLI using Ink
  • Mobile responsive fixes with automated tests

The Great Migration

Deployment is where things got interesting. I started with GitHub Pages, moved to Netlify, and finally settled on Cloudflare Pages.

Each migration taught me something. Netlify was easy but limited. Cloudflare gave me the edge performance and configuration I needed.

The Build System Revolution

The build script was getting unwieldy. At 1,704 lines, it was a monster. So I did what any sane developer would do: I split it up.

Now the largest file is 328 lines. Each module has a single responsibility:

  • build/config.js — Marked configuration with syntax highlighting
  • build/content.js — Content loading and frontmatter parsing
  • build/series.js — Series collection and page building
  • build/feeds.js — Atom feed, sitemap, robots.txt

The TUI Adventure

This is where things got fun. I didn't want a boring terminal output during development. I wanted something beautiful.

A blank terminal window beside a game controller

Enter Ink—React for the command line. Building a TUI (Text User Interface) for the dev server became an obsession.

The result? A development experience that includes:

  • Live reload with WebSocket connections
  • QR codes for mobile testing
  • Cloudflared tunneling for public URL access
  • A debug panel showing real-time logs
  • Keyboard shortcuts for everything

But it wasn't smooth sailing. Ink v6 broke everything. Older Ink helper packages used require() which doesn't work with Ink v6's top-level await. I had to implement custom components—a Link component using ANSI OSC 8 escape codes, a custom dimension hook, everything.

Was it necessary? Probably not. Was it worth it? Absolutely.

From the studio: The same care for tooling goes into client work: the studio builds Kotlin Multiplatform apps for iOS and Android, along with the build and release engineering under them.

The PWA Journey

I wanted my site to work offline. Not just "show a cached page" offline, but properly offline with smart caching strategies.

The service worker uses:

  • Cache-first for static assets (CSS, JS, images, fonts)
  • Network-first for HTML pages with offline fallback
  • Stale-while-revalidate for CDN resources

And when there's an update? A dismissible notification with a 24-hour cooldown, following Google's PWA patterns.

The Polish Phase

The final stretch was all about polish. Accessibility fixes. Typography improvements. OG image generation with CTA badges.

I even built a GitHub contribution graph as a footer widget. Then removed it. Some experiments don't make the cut, and that's okay.

What I Built

Here's what exists today:

A machine turning plain pages into finished web pages

Core Features:

  • Markdown content with frontmatter
  • Blog posts with tags and series support
  • Syntax highlighting (Android Studio theme)
  • Dark/light mode toggle
  • Configurable image sizes in markdown
  • Open Graph image generation with Satori
  • RSS/Atom feed

Developer Experience:

  • Interactive TUI dev server with Ink
  • Live reload via WebSocket
  • QR codes for mobile testing
  • Cloudflared tunneling for public access
  • Debug panel for monitoring
  • Content manager CLI

Production:

  • CSS class obfuscation
  • HTML/CSS/JS minification
  • Service Worker with smart caching
  • PWA update notifications
  • Comprehensive testing (a11y, responsive, unit, deployment)
  • Cloudflare Pages deployment

What I Learned

An open notebook under a light bulb, with a pencil

1. You Don't Need a Framework

For a personal blog, a custom static site generator is not only viable—it's liberating. No fighting configuration. No mysterious build errors. Just code you wrote and understand.

2. Claude is a Game-Changer

As a Kotlin developer with limited JavaScript experience, Claude was invaluable. Not for writing code blindly, but for understanding patterns, debugging issues, and learning best practices. Every feature was a collaboration.

3. Developer Experience Matters

I spent way too much time on the TUI. Fixing spinner flickering. Implementing custom Ink components. Adding keyboard shortcuts. But every time I run npm run dev and see that beautiful terminal interface, I smile. That matters.

4. Simplicity is Hard

The hardest part wasn't adding features—it was knowing when to stop. The GitHub contribution graph was cool. But it didn't serve the purpose of the site. Removing it was the right call.

5. The Journey is the Destination

This project isn't "done." It never will be. And that's the point. It's a living thing that grows with me, that I can shape and improve forever.

The End (For Now)

Nine years. Jekyll to Hugo to Nuxt to Next.js to... this. A static site generator I built myself, with help from an AI, using a language I barely know.

Sometimes the longest journeys bring you right back home. But you're not the same person who left.


This site is built with love, frustration, and determination. (The site has since moved to Next.js; this post describes the generator as it was.) If you're interested in how something specific works, feel free to reach out.

All writingNextMaking SDKMAN Java Work with macOS: A One-Line Fix