Home

This post describes how I added a hot-reload feature to my blogging workflow.

This blog is made up of

Commands to build and deploy is codified in a Makefile:

This workflow has been working well, but I felt that things can be improved by adding some sort of live preview. I would like to be able to open my Markdown files alongside the HTML page, edit my post, and see changes live.

Hot reload

The simplest way would be to insert some sort of JS that does a page refresh every 5s. It might seem sort of a waste, but it will work. The problem is that 5s might be too long - Pandoc compilation takes much less than 1s, and it would take less than 5s for me to manually refresh.

So instead, we need a way for a local server to inform the webpage that something changed, and should be reloaded.

This is a very common feature present in many development environments, Flutter, React Native, and many JS front-end environments. This usually goes by the name hot reload. Note that these are much more complicated, in that they hot-swap specific parts (components) that changed, without reloading the entire page. For my blog, reloading the entire page is just fine.

WebSockets

For this, we can use WebSockets. We will need:

I chose to use uWebSockets 1, which has an example EchoServer built in - all it does is echo whatever the client sent:

# make sure to do recursive, it has a submodule which you need to pull in
git clone --recursive [email protected]:uNetworking/uWebSockets.git
make examples
./EchoServer

With the server up at “localhost:9001”, we now need a client to do a test. I wrote up a simple HTML page, following instructions on MDN’s Writing WebSocket client applications.

<html>
  <head>
    <script>
      var s = new WebSocket("ws://localhost:9001");
      s.onopen = function(event) {
        s.send("Hello world!");
      }
    </script>
  </head>
</html>

The important part here is to call s.send inside the callback s.onopen, since establishing a WebSocket connection is asynchronous. I missed that initially and got no response from the server.

You should then be able to see “Hello world!” sent from the page to the server, and back 2.

Listening to file changes

We can now communicate from the server to our webpage, but the echo server can only do so via a message from the client (hence echo). What we need to do now is to change our Make+Pandoc build to notify the WebSocket server that it should send a message to the client.

There are many ways to do this. While browsing the examples in uWebSockets, I found misc/main.cpp, which showed how you can define HTTP endpoints and WebSocket endpoints listening on the same port. This makes sense, because a WebSocket connection always starts with a HTTP connection with special headers, and then that get upgraded into a WebSocket connection.

So that’s what we will use:

The tricky part is here to store the WebSocket object in the server. For simplicity, I chose a single global pointer (not exactly global, since it’s still within main, but funtions as a global). This means that I can only have a single active WebSocket connection that I can ask to refresh the page, but is fine for my use case now.

int main() {
  uWS::WebSocket<false, true> *global_ws = nullptr;

  uWS::App()
    .get("/", [&global_ws](auto *res, auto *req) {
        if (global_ws != nullptr) {
          global_ws->send("refresh", uWS::OpCode::TEXT);
        }
        res->end("");
    })
    .ws<PerSocketData>("/*", {
      // more configuration
      .open = [&global_ws](auto *ws) {
        /* Open event here, you may access ws->getUserData() which points to a PerSocketData struct */
        global_ws = ws;
      },
      .close = [&global_ws](auto *ws, int code, std::string_view message) {
          global_ws = nullptr;
      }
    }).listen(9001, [](auto *token) {
        if (token) {
            std::cout << "Listening on port " << 9001 << std::endl;
        }
    }).run();
}

The second part is that I wouldn’t want this JS snippet to be in my final HTML that goes live, only for development. So I will need to have a separate template that Pandoc uses for local builds. That’s not too hard, just a bit of duplication. I chose to create a separate “dev” directory and will recompile all my posts, then add “dev” to .gitignore.

Keepalive

Aka “why does my websocket connection close automatically?” (because I literally Googled this). This is a somewhat nasty problem I faced before, while working on a school project that used WebSockets to build a chat application.

Browsers will terminate WebSocket connections after a period of inactivity (probably to save resources). So, I was seeing my hotreload work for a couple of saves, then it stopped working. The fix here is to register a function using setInterval to send keepalive messages to the server every 5 seconds:

var intervalId;
// Ping every 5s to keep this WS connection alive.
// Otherwise the browser will terminate this connection.

function keepalive(s) {
  intervalId = setInterval(function() {
    s.send("keepalive");
  }, 5000);
}

var s = new WebSocket("ws://localhost:9001");
s.onopen = function(even) {
  console.log("WebSocket connection open.");
  keepalive(s);
};
s.onclose = function(even) {
  console.log("WebSocket connection closed.");
  clearInterval(intervalId);
};
s.onerror = function(event) {
  console.error("WebSocket error observed:", event);
  clearInterval(intervalId);
};
s.onmessage = function(event) {
  if (event.data === "refresh") {
    location.reload();
  }
};

Clearing intervalId is not required for this small project, but is good habit anyway.

Stopping the hotreload server

The problem with the current setup is that, I’m starting the server as a backgrounded task, which means that when I Ctrl-C on my terminal, it is left in the background.

It is not a huge deal, since when I terminate the shell session it goes away, but it is a bit annoying.

I remember reading about trap in bash, which is a way to catch signals and respond to it. I found a useful guide, which suggests using a special EXIT signal that can be use to do some cleanup right before a bash script exits.

So the simple fix is to create a helper script that:

You can see the changes on Github. “hotreload.cpp” is the server, lightly modified from the example “EchoServer”. The Makefile contains some hardcoded paths to the headers and object files for uWebSockets, so if you want to use it, you’ll need to modify it.


  1. I’ve been doing C++ development recently, and wanted to expose myself to more C++ projects.↩︎

  2. You can observe WebSocket messages in the Chrome DevTools.↩︎