Adding Hot Reload to Hexo

A lightweight script that makes the Hexo dev server auto-refresh the browser whenever you save a post.

hexo server natively watches for file changes and regenerates pages, but unfortunately it never tells the browser to refresh. After every edit, we still have to switch back to the browser and hit F5 or ⌘ + R.

I had used Astro before, and it could do this. Since it was clearly feasible, I asked CodeX to produce a script: user updates a .md file -> Hexo regenerates the page -> the browser automatically refreshes the current http://localhost:4000 page.

I’ve never studied frontend systematically, but from using tools like Astro and Vite I’d already experienced this “hot reload” feature, so I was fairly sure the idea was doable. I asked CodeX to write a script that implements: user updates .md -> Hexo regenerates the page -> browser refreshes automatically.

Experiences: Wish I Had This Sooner

With the script in place, I can now split my screen: editing the md file on the right, and Edge/Chrome/Zen/Safari showing a live preview on the left. The improvement to the experience boils down to:

  1. Most directly, no more manual refreshing. Since the blog couldn’t hot-reload in the browser before, I usually made do with the editor’s built-in preview
  2. Previewing in the browser is true, out-of-the-box WYSIWYG

WYSIWYG: Browser or Editors?

On the WYSIWYG point, mainstream editors all ship with Markdown preview these days, but the results are underwhelming. Typora looks nicer, but its editing experience falls a bit short of Obsidian/VSCode/NeoVim/Zed.

Also, in pursuit of better UI/UX, I often stitch HTML code into my Markdown. Most editors’ preview engines choke on these custom tags and fail to render them correctly, which of course isn’t the editors’ fault.

Some cases that fail to render correctly:

  • Shiki syntax-highlighting Transformers: a plugin in Obsidian can partially render them, but its syntax follows the Expressive Code standard
  • Some markdown-it plugins: popular ones like callout/math/mermaid preview fine in Obsidian/Typora, but the less common yet very practical tab component doesn’t. Obsidian’s wikilinks have the same problem
  • Custom HTML Elements (Web Components): see this post for how custom HTML Elements are used; the components defined on this site are showcased on this page
  • CSS styles embedded in Markdown can’t be previewed either

Appendix: Source Code

Hexo automatically loads scripts placed in scripts/ under the project root or the theme directory. I chose to create scripts/hot-reload.js in the theme’s /scripts directory

Since GitHub offers a better code-reading experience and stays more up to date, I won’t paste the code here

Appendix: How It Work

Since this was vibe-coded, naturally I have no idea how it works; as long as it works 🤓

Just ask the AI for a straight, no-dodging explanation, do we even need one?

BTW, the output has been lightly edited and trimmed; the model I actually asked was Gemini in AI Studio, but its logo is too ugly, so I’m swapping in OpenAI’s logo for now

Explain how this works and what technologies it uses
Core Technology: SSE (Server-Sent Events)

At its core, the script uses SSE (Server-Sent Events), a technology that lets the server push real-time notifications to the browser.

How It Works
  1. Environment detection (isHexoServerCommand)

    The script first checks whether it is running under hexo server mode. This ensures that these development-only bits are not injected during a static hexo generate build or a production deployment.

  2. Listening to the Hexo lifecycle

    • hexo.on('generateAfter'): fires when Hexo finishes regenerating pages. Every time Hexo completes a regeneration, the script triggers a refresh. Since the Hexo server automatically regenerates on file changes, listening to this single event is enough.
  3. Debounce & Cooldown

    To prevent frequent editor saves (such as auto-save) from making the browser refresh like crazy, the script adds a 150 ms debounce and a 500 ms cooldown interval, keeping refreshes both timely and stable.

  4. Middleware injection (server_middleware)

    The script opens a special “secret handshake” endpoint inside Hexo’s built-in server: /__hexo_live_reload. The browser stays connected to this endpoint, waiting for the server’s signal.

  5. Frontend injection (injector)

    Using Hexo’s injector feature, the script automatically inserts a tiny piece of JavaScript at the end of every page’s <body>. This code listens for the reload signal from the server and calls window.location.reload() when it arrives.

    What is window.location.reload? What is __hexo_live_reload?

    1. What is window.location.reload()?

    This is a method provided by the browser’s native JavaScript, and what it does is very straightforward: it refreshes the current page.

    • Effect: Equivalent to pressing F5 on your keyboard, or clicking the “refresh” button next to the browser’s address bar.
    • Role in the script: When the browser receives the “file updated” message from the server, it runs this line. You no longer have to click refresh yourself; the page automatically updates to the latest state.
    2. What is /__hexo_live_reload?

    This is a virtual communication address (Endpoint).

    In a normal Hexo preview, visiting http://localhost:4000 shows your blog. But this script quietly opens a hidden “private line” in the background: http://localhost:4000/__hexo_live_reload.

    Here is how it works:

    • It’s a “pipe”: This address doesn’t exist on your disk. It is a long-lived connection channel (based on SSE) created by the Node.js script intercepting requests.

    • Server side (Hexo): When you see req.url === LIVE_RELOAD_PATH in scripts/live-reload.js, it is telling Hexo: “If a browser requests this address, don’t serve it page content — keep the connection open and stand by to send signals.”

    • Client side (browser): The script puts new EventSource('/__hexo_live_reload') into the page. This line tells the browser to keep “listening” on this address, like waiting by the phone.


    Summary: How Do the Two Work Together?
    1. Connect: The browser opens your blog and automatically connects to the /__hexo_live_reload pipe.

    2. Wait: The browser waits at the other end of the pipe; nothing happens.

    3. Trigger: You edit a .md file, and Hexo regenerates the page.

    4. Signal: The Hexo server pushes a message into the /__hexo_live_reload pipe: {"type": "reload"}.

    5. Action: The browser receives the message on its end and immediately calls window.location.reload().

    6. Result: The page is fresh.