Reload a webpage when a file changes
I store my blog in a git repo of plain Markdown files. It’s a pretty spartan setup, which I like — it forces me to keep all my writing extremely portable between website generators.
To preview the files in my browser, I render the Markdown with Caddy. I wanted to add live reloading when I made a change, which was tricky because Caddy is just acting as a plain file server here. With the help of ChatGPT, I was able to come up with a solution involving a bunch of shell scripts.
The general workflow:
- Create a temporary file using mktemp.
- Use the network utility socatto listen to network requests, setting up a server-sent events connection and sending any lines appended to the file.
- Use entrto append events to the temporary file whenever a watched file changes.
- On the webpage, listen for server-sent events and reload the page when one comes through.
- Use trapto clean up the temporary file when the process exits.
Let’s go through the steps one by one.
First, we run mktemp to make a temporary file, saving the path in a variable called $TMPFILE:
TMPFILE=$(mktemp)Next, we set up step 5 using trap to remove $TMPFILE when the process exits:
trap 'rm -f $TMPFILE; exit' INT TERM EXITNow we’re getting to the meat of things. We use socat to listen for requests on port 8081. When a client connects, we initially responds with HTTP/1.1 200 OK and a bunch of headers, then keep the connection open and tail any lines appended to $TMPFILE.
socat TCP-LISTEN:8081,fork,reuseaddr SYSTEM:' \
  echo HTTP/1.1 200 OK; \
  echo Content-Type\: text/event-stream; \
  echo Cache-Control\: no-cache; \
  echo Connection\: keep-alive; \
  echo; \
  tail -n0 -F $TMPFILE'The -n0 flag to tail makes it start at the end of $TMPFILE, so the client only receives events sent after opening the connection.
While that’s running, we run entr, appending reload events to $TMPFILE whenever a watched file changes. In this case, it’s watching files that are tracked by git.
git ls-files | entr -s 'echo "event: reload\ndata:\n" >> $TMPFILE'One last important thing: these all need to run concurrently. You can do that by chaining them together using &.
ChatGPT tells me that I need to run wait at the end of the & chain to ensure that socat and entr don’t quit prematurely, but in my testing that doesn’t seem to matter.
Here’s the full shell script:
# Create a temporary file
TMPFILE=$(mktemp)
# Set the trap to remove the temporary file on INT, TERM and EXIT
trap 'rm -f $TMPFILE; exit' INT TERM EXIT
# Start the server-side events server and tail lines from the temporary file
socat -v -v TCP-LISTEN:8081,fork,reuseaddr SYSTEM:'echo HTTP/1.1 200 OK; echo Content-Type\: text/event-stream; echo Cache-Control\: no-cache; echo Connection\: keep-alive; echo; tail -n0 -F '"$TMPFILE"'' &
# Append lines to the temporary file whenever a file tracked by git changes
git ls-files | entr -s 'echo "event: reload\ndata:\n" >> '"$TMPFILE"'' &
# Wait for background processes to finish
waitOn the webpage side, we can connect to the server, listen for reload events and reload the page when we receive one:
new EventSource("http://localhost:8081").addEventListener("reload", () => location.reload());