Live Reload
Live reload watches your project files and refreshes the browser when changes are detected. CSS changes are hot-swapped without a full page reload, while other file types trigger a full refresh. It uses a WebSocket connection between the server and browser — no browser extension required.
Three components
Section titled “Three components”Live reload consists of three parts that work together:
liveReloadmiddleware — injects a small client script into HTML responses.liveReloadWsWebSocket handler — maintains the WebSocket connection at/__pidgn/live-reload.- swatcher file watcher — monitors directories for changes and broadcasts reload messages via PubSub.
-
Add the middleware and WebSocket route
const App = pidgn.Router.define(.{.middleware = &.{pidgn.errorHandler(.{}),pidgn.logger,pidgn.gzipCompress(.{}),pidgn.liveReload(.{}), // AFTER gzipCompresspidgn.staticFiles(.{ .dir = "public", .prefix = "/static" }),},.routes = &.{pidgn.Router.get("/", index),pidgn.Router.websocket("/__pidgn/live-reload", .{.handler = pidgn.liveReloadWs(),}),},}); -
Start the file watcher in your main function
const live_reload = @import("live_reload");pub fn main() !void {var gpa = std.heap.GeneralPurposeAllocator(.{}){};defer _ = gpa.deinit();try live_reload.startWatcher(gpa.allocator());var server = pidgn.Server.init(gpa.allocator(), .{.port = 4000,}, &App.handler);try server.listen(std.io.defaultIo());} -
Configure swatcher in build.zig
Add swatcher as a dependency and pass a build option to enable live reload conditionally:
const live_reload = b.option(bool, "live_reload", "Enable live reload") orelsestd.mem.eql(u8, env, "dev");if (live_reload) {const swatcher_dep = b.dependency("swatcher", .{ .target = target, .optimize = optimize });exe.root_module.addImport("swatcher", swatcher_dep.module("swatcher"));}
Middleware placement
Section titled “Middleware placement”Place liveReload after gzipCompress in the middleware pipeline. The middleware injects a <script> tag before the closing </body> of HTML responses, so it needs to see the uncompressed response body. If placed before gzip, the body would be compressed and the injection point would not be found.
How it works
Section titled “How it works”- The
liveReloadmiddleware intercepts HTML responses and injects a client-side script before</body>. - The script opens a WebSocket connection to
/__pidgn/live-reload. - The
liveReloadWshandler subscribes each connection to the"__live_reload"PubSub topic. - The swatcher file watcher monitors configured directories (e.g.
public/) for changes. - When a file changes, the watcher broadcasts a message to the PubSub topic.
Message types
Section titled “Message types”| Message | Trigger | Browser behavior |
|---|---|---|
{"type":"css"} | A .css file changed | Reloads all <link> stylesheets by appending a ?_lr=<timestamp> query parameter. No full page reload. |
{"type":"reload"} | Any other file changed | Full page reload via location.reload(). |
Auto-reconnection
Section titled “Auto-reconnection”The client script automatically reconnects with exponential backoff (100ms to 5000ms). If the connection was lost for more than 1 second before reconnecting, a full page reload is triggered — this handles the case where the server restarted and files may have changed while disconnected.
LiveReloadConfig
Section titled “LiveReloadConfig”| Option | Type | Default | Description |
|---|---|---|---|
endpoint | []const u8 | "/__pidgn/live-reload" | WebSocket endpoint path for the reload connection. |
File watcher configuration
Section titled “File watcher configuration”The swatcher watcher is configured in your live reload module:
pub fn startWatcher(allocator: std.mem.Allocator) !void { var watcher = try swatcher.Watcher.init(allocator, .{ .poll_interval_ms = 50, .coalesce_ms = 200, .dirs = &.{"public/"}, .watch_mode = .WATCH_DIRECTORIES, }); // On change, broadcast to PubSub topic "__live_reload"}| Option | Value | Description |
|---|---|---|
poll_interval_ms | 50 | How often to poll for file changes (milliseconds). |
coalesce_ms | 200 | Debounce window — changes within this period are batched into a single reload. |
watch_mode | WATCH_DIRECTORIES | Watch directory-level changes only, avoiding duplicate events from individual file watches. |
Build flag pattern
Section titled “Build flag pattern”Use a comptime build flag to strip live reload from production builds:
const build_options = @import("build_options");
const App = pidgn.Router.define(.{ .middleware = &.{ pidgn.errorHandler(.{}), pidgn.logger, if (comptime build_options.live_reload) pidgn.liveReload(.{}) else null, pidgn.staticFiles(.{ .dir = "public", .prefix = "/static" }), }, .routes = &.{ pidgn.Router.get("/", index), if (comptime build_options.live_reload) pidgn.Router.websocket("/__pidgn/live-reload", .{ .handler = pidgn.liveReloadWs(), }) else null, },});This ensures no live reload code is included in production binaries.
Next steps
Section titled “Next steps”- Asset Pipeline — bundle and fingerprint assets
- Dev Server — automatic rebuilds on source changes
- Assets CLI —
pidgn assets watchfor rebuilding assets on change - Static Files — serve files from disk
- WebSockets — the WebSocket system that live reload is built on