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:
- 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
- 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.
The script is built around SSE (Server-Sent Events), a technology that allows servers to push real-time notifications to browsers.
- Environment Detection (
isHexoServerCommand)The script first checks whether it is running in
hexo servermode. This ensures that the development-only code is not injected when runninghexo generatefor static generation or deploying to production. - 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.
- Debounce & Cooldown
To prevent frequent file saves from the editor (such as auto-save) from causing excessive browser refreshes, the script adds a
150 msdebounce period and a500 mscooldown interval, ensuring that refreshes are both responsive and stable. - 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. - Frontend Injection (
injector)Through Hexo's
injectorfeature, the script automatically inserts a small piece of JavaScript at the end of every page's<body>. This code listens forreloadsignals from the server and callswindow.location.reload()when a signal is received.
What are window.location.reload and __hexo_live_reload?
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.
/__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_PATHinscripts/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.
- Connection: The browser automatically connects to the
/__hexo_live_reloadchannel when opening the blog. - Waiting: The browser waits on the other end of the channel while nothing happens.
- Trigger: You modify a
.mdfile, and Hexo regenerates the page. - Notification: The Hexo server sends a message through the
/__hexo_live_reloadchannel:{"type": "reload"}. - Action: The browser receives the message and immediately executes
window.location.reload(). - Result: The page is refreshed and displays the latest content.