Why I'm building Stati

I rebuilt this blog on a new framework every few years and the blog never changed. So I wrote a static site generator I intend to keep for ten years.

GRD 036 #A42 Filed webdev stati

Contents
  1. The itch
  2. The plan
  3. Where it is now
  4. What I want it to be
  5. What's next

In 2020 I rebuilt this blog on Nuxt Content and wrote about it. The old Hungarian one ran on Jekyll. Both times the posts were the same Markdown files, the design barely moved, and I still spent weeks learning a framework's opinion about what a blog is.

Then the framework moved on. Major versions, a module that got rewritten, a content layer with a new query API. The blog didn't change, the thing under it did, and every time I wanted to write a post I first had to remember how the thing under it worked.

I wrote back then that every JS framework has its own static site generation option, so the best bet is to stick with something you like. Five years later I have a correction: they are frameworks first and generators second. Static output is a mode they support, not the reason they exist. If all you want is Markdown in and HTML out, you are carrying the whole framework for a feature it treats as a checkbox.

The itch

Here is the list I kept coming back to. It's not a long one.

  • A template language that is HTML with a little JavaScript in it. No JSX, no components that need hydrating, nothing shipped to the browser that I didn't write myself.
  • A config file in TypeScript with autocomplete, so I don't look up the options every time.
  • A dev server that rebuilds the page I'm looking at and reloads it, and doesn't need a restart every hour.
  • The boring things every content site needs: a feed, a sitemap, meta tags, a search index. Built in and switched on with one line, not collected from six plugins with three maintainers between them.
  • A build that only touches what changed. My posts from 2020 don't need re-rendering every time I fix a typo in a new one.

Every generator I looked at had most of this list. None had all of it without a framework attached.

So in September 2025 I did the thing you're not supposed to do. Fine, I'll build my own static site generator. With blackjack and incremental builds. In fact, forget the blackjack.

The plan

The first commit was on September 5. The first release was on September 8. I'm not saying that to brag, I'm saying it because the plan was small enough that a usable first version fit in three days.

The concept doc from day one said:

  1. TypeScript, the whole thing. The config goes through defineConfig() and is typed end to end.
  2. Eta for templates. It's small, it's fast, and a layout is a complete HTML document you can read top to bottom.
  3. markdown-it for content, front matter from gray-matter.
  4. The filesystem is the router. site/about.md is /about. A layout.eta in a folder is the layout of everything below it. Partials live in _partials/.
  5. Incremental Static Generation at build time. A cache manifest, a TTL per page, aging rules that rebuild new posts often and old ones rarely, and stati invalidate "tag:news" for when you need to.
  6. A scaffolder, npx create-stati, so a new site is one command.

Stati is "static" with the end bitten off, which is roughly how much ceremony I wanted left in it.

Two things from that first doc are gone. Vite was in it as the dev server and left in the second week, because a generator that writes HTML files doesn't need a module graph in the middle. And there was a WYSIWYG Markdown editor at /_editor. I never wrote it and I have not missed it once.

Where it is now

Five months in, this is the kind of file a site has, and all of it is typed:

typescript
// stati.config.ts
import { defineConfig } from "@stati/core";

export default defineConfig({
  site: { title: "Field Notes", baseUrl: "https://example.com" },
  isg: { ttlSeconds: 21600 },
  search: { enabled: true },
  sitemap: { enabled: true },
  robots: { enabled: true },
  typescript: { enabled: true },
});

Everything in there comes from Stati's own packages: ISG with its cache, SEO tags injected into every page, sitemap and robots.txt, RSS feeds, a JSON search index, client TypeScript bundled with esbuild and injected only on the pages that need it, callable partials with props, navigation helpers, build hooks, a dev server with an error overlay in the browser, and build metrics for when you want to know where the time went. Core has a handful of runtime dependencies, and I count them every time I add one.

The docs at stati.build are built with Stati, because a generator that can't build its own docs has no business building yours.

This blog is still on Nuxt while I write this. Moving it is on the list, and when it moves it will be a good test, because this blog has opinions: three colours per post, a catalogue number, a 16-colour palette and pixel art that must not be resampled. If Stati can't do my blog without a fork, it isn't done.

What I want it to be

The motto is: install it once, write Markdown for ten years.

  • A site is a folder of Markdown and a couple of .eta files. You can read all of it in an afternoon, and so can whoever maintains it after you.
  • Everything a content site needs is in the box, off by default, and turning it on is one typed line.
  • Builds only touch what changed, in CI too, so a thousand-page site doesn't pay for a typo.
  • Nothing runs in the browser unless you wrote it. The output is files, and any host that serves files can serve it.
  • It stays small enough that one person can keep the whole thing in his head. That person is me, and I'd like to keep it that way for a while.

What's next

Pagination for long listings and a page for every tag, which this blog needs before it can move. A proper story for images, probably as a separate package so core stays lean. And then the move itself, which will be its own post, because I know how these go.

Bugs will be frequent and the documentation will lack depth, as I wrote about fresh tech in 2020. This time at least I know who to blame.