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 the working principle and the technologies used.

Core Technology: SSE (Server-Sent Events)

The script is built around SSE (Server-Sent Events), a technology that allows servers to push real-time notifications to browsers.

Implementation Flow
  1. Environment Detection (isHexoServerCommand)

    The script first checks whether it is running in hexo server mode. This ensures that the development-only code is not injected when running hexo generate for static generation or deploying to production.

  2. Listening to the Hexo Lifecycle
    • hexo.on('generateAfter'): Listens for the completion of Hexo page generation. Every time Hexo finishes generating pages, the script triggers a refresh. Since Hexo server automatically regenerates pages after file changes, listening to this event is sufficient.
  3. Debounce & Cooldown

    To prevent frequent file saves from the editor (such as auto-save) from causing excessive browser refreshes, the script adds a 150 ms debounce period and a 500 ms cooldown interval, ensuring that refreshes are both responsive and stable.

  4. Middleware Injection (server_middleware)

    The script creates a special endpoint inside Hexo's built-in server: /__hexo_live_reload. The browser keeps a connection to this endpoint and waits for commands from the server.

  5. Frontend Injection (injector)

    Through Hexo's injector feature, the script automatically inserts a small piece of JavaScript at the end of every page's <body>. This code listens for reload signals from the server and calls window.location.reload() when a signal is received.

What are window.location.reload and __hexo_live_reload?

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

This is a native JavaScript method provided by browsers. Its purpose is straightforward: reload the current page.

  • Effect: It is equivalent to pressing the F5 key or clicking the browser's refresh button.
  • Purpose in the script: When the browser receives a message from the server indicating that files have been updated, it executes this method. This eliminates the need to manually refresh the page, allowing it to update automatically.
2. What is /__hexo_live_reload?

This is a virtual communication endpoint.

In a normal Hexo preview, visiting http://localhost:4000 displays your blog. However, this script quietly creates a hidden communication channel in the background: http://localhost:4000/__hexo_live_reload.

Its working principle is as follows:

  • It is a "pipe": This address does not exist as a file on your disk. It is created by the Node.js script intercepting requests and provides a long-lived connection channel based on SSE.
  • Server side (Hexo): When you see req.url === LIVE_RELOAD_PATH in scripts/live-reload.js, it means: "If a browser requests this address, do not return webpage content. Instead, keep the connection open and wait to send signals."
  • Client side (Browser): The script creates new EventSource('/__hexo_live_reload') in the page. This tells the browser to continuously listen to this endpoint, similar to waiting for a phone call.
Summary: How Do They Work Together?
  1. Connection: The browser automatically connects to the /__hexo_live_reload channel when opening the blog.
  2. Waiting: The browser waits on the other end of the channel while nothing happens.
  3. Trigger: You modify a .md file, and Hexo regenerates the page.
  4. Notification: The Hexo server sends a message through the /__hexo_live_reload channel: {"type": "reload"}.
  5. Action: The browser receives the message and immediately executes window.location.reload().
  6. Result: The page is refreshed and displays the latest content.