Modern web development has spoiled us with instant feedback. Change a line of CSS, and the browser updates instantly. Modify a component, and you see the result immediately. This isn't a luxury. It's a fundamental requirement for productive development.

Hot-reloading is table stakes for static site generators. Hugo, Jekyll, Eleventy, Astro, and Next.js all have it built-in. It's expected, not exceptional.

Gesttalt now has it too. Change a file, and your browser updates automatically. No configuration, no plugins, no extra steps. It just works.

This isn't groundbreaking. It's catching up to where the ecosystem already is. But how you implement hot-reloading matters, especially for a project built on simplicity and portability.

How It Works

The implementation is straightforward because it doesn't try to be clever. The file watcher monitors four locations:

  • content/ - Your Markdown files
  • theme/ - Mustache templates and partials
  • static/ - CSS, images, and other assets
  • gesttalt.toml - Site configuration (TOML format)

Every 500 milliseconds, it checks modification times. When something changes, it rebuilds the site and signals the browser to reload. The browser polls a simple JSON endpoint at /api/check-reload that returns either {"reload":true} or {"reload":false}.

This polling approach is less sophisticated than file system events like inotify or FSEvents. But it has a critical advantage: it works everywhere. Linux, macOS, Windows, all with zero platform-specific code. No dependencies beyond Zig's standard library.

Why Polling, Not Events

Platform-specific file watching APIs are tempting. They're faster, more efficient, and feel more "correct." But they come with costs that don't make sense for Gesttalt.

Using inotify on Linux, FSEvents on macOS, and ReadDirectoryChangesW on Windows means maintaining three separate implementations. It means testing on three platforms. It means potential bugs that only show up on one system.

For a development server checking a handful of directories, polling every half second is imperceptible. The rebuild takes longer than the polling interval. The browser refresh takes longer than the polling interval. The complexity isn't worth the microseconds.

This decision aligns with Gesttalt's broader philosophy: choose simplicity and portability over premature optimization. The web didn't need the fastest possible JavaScript engine to succeed. It needed one that worked the same way everywhere.

The Client-Side Script

The hot-reload functionality requires a small JavaScript snippet in every HTML page. During development, Gesttalt automatically injects this before the closing </body> tag:

(function() {
  let checkInterval = 500;
  async function checkForReload() {
    try {
      const res = await fetch('/api/check-reload');
      const data = await res.json();
      if (data.reload) {
        console.log('[Hot Reload] Site rebuilt, reloading...');
        window.location.reload();
      }
    } catch(e) {
      // Silently ignore errors
    }
  }
  setInterval(checkForReload, checkInterval);
  console.log('[Hot Reload] Watching for changes...');
})();

This script only exists in development. Production builds are clean HTML with no extra JavaScript. The injection happens at response time, not build time, so there's no risk of the development code leaking into your published site.

Thread Safety Matters

The file watcher runs in a separate thread from the HTTP server. This means two pieces of code need to coordinate: one watching for changes, one answering browser requests. Get this wrong, and you have race conditions.

Gesttalt uses a simple mutex-protected state structure:

const HotReloadState = struct {
    mutex: std.Thread.Mutex = .{},
    should_reload: bool = false,

    fn signalReload(self: *HotReloadState) void {
        self.mutex.lock();
        defer self.mutex.unlock();
        self.should_reload = true;
    }

    fn checkAndReset(self: *HotReloadState) bool {
        self.mutex.lock();
        defer self.mutex.unlock();
        const result = self.should_reload;
        self.should_reload = false;
        return result;
    }
};

When the file watcher detects a change and completes the rebuild, it calls signalReload(). When the browser polls /api/check-reload, the server calls checkAndReset(), which atomically checks the flag and clears it. Clean, simple, correct.

What This Enables

Hot-reloading isn't just about convenience. It changes how you work.

You can keep your editor and browser side-by-side, making changes and seeing results without breaking focus. You can iterate on design faster because the feedback loop is immediate. You can catch mistakes quickly because you see them the moment they happen.

This matters more for static sites than it might seem. When you're working with templates, the relationship between code and output isn't always obvious. Small changes can have surprising effects. Instant feedback turns debugging from a frustrating hunt into a quick iteration cycle.

The Cost of Simplicity

There are trade-offs. The current implementation rebuilds the entire site on every change, even if you only modified one blog post. It doesn't preserve scroll position when the browser reloads. It doesn't do partial updates for CSS changes.

These are solvable problems. But they're not solved yet, and that's intentional.

Gesttalt's development philosophy is to get the simple version working first, then optimize based on real usage. Hot-reloading works. It's fast enough for real development. The incremental improvements can come later if they're actually needed.

This approach follows the principle of "worse is better" from Richard Gabriel's essay. A simple, working solution beats a complex, perfect one. You can always add sophistication later. You can't remove it.

What's Next

The foundation is solid, but there are obvious improvements to consider:

  • Partial rebuilds - Only regenerate changed content instead of the whole site
  • Smarter reloading - Preserve scroll position and form state across refreshes
  • CSS injection - Update styles without a full page reload
  • Error overlay - Show build errors directly in the browser

None of these are urgent. Hot-reloading works well enough that these feel like polish, not requirements. We'll add them when they become genuine pain points, not because they sound nice.

Try It Yourself

Hot-reloading is available now in Gesttalt's development server. Start the dev server with:

gesttalt dev

Edit any file in content/, theme/, or static/. Watch your browser update automatically. That's it.

No configuration files. No plugin installation. No webpack. Just the development experience you should have had all along.