Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nuejs/nue/llms.txt

Use this file to discover all available pages before exploring further.

Nue’s development server applies changes to the browser immediately after you save a file — not by reloading the whole page, but by pushing the changed asset and letting the client apply it surgically. A CSS edit updates only the stylesheet node. A Markdown change updates only the article content. A component change updates only the component instances. This is universal hot reload: every file type in the project participates, without any per-file configuration.

What universal hot reload covers

Unlike framework-specific HMR that only handles JavaScript modules, Nue’s hot reload covers every file type that can change during development:

Content

Markdown files (.md) re-render and push updated article HTML. The page structure stays intact; only the content region updates.

CSS

Stylesheets update in-place without a page reload. The browser applies the new styles immediately, preserving scroll position and DOM state.

Layouts and components

Nue HTML templates re-render and push new component output. Client-side dynamic components (.dhtml) recompile and reinitialize.

Data files

YAML and JSON data files in @shared/data/ and application directories re-trigger renders for pages that depend on them.

Server routes

Files in @shared/server/ are re-imported on the next request when they change. No server restart required.

Configuration

site.yaml changes are parsed and broadcast to the browser, updating global settings like colors and navigation without rebuilding.

How it works

The development server combines a file watcher, an in-memory asset graph, and a WebSocket broadcast channel. When a file changes, the server re-renders it, attaches the new content, and broadcasts a message to all connected browser tabs.

File watching

The fswatch function in tools/fswatch.js uses Node’s fs.watch with { recursive: true } to monitor the entire project tree. It filters out ignored paths (node_modules, dotfiles, lock files) and editor backup files (.~, .bck):
// From tools/fswatch.js
export function fswatch(root, opts = {}) {
  const { ignore = ['.*', '_*', 'node_modules'] } = opts

  const watcher = watch(root, { recursive: true }, async function(event, path) {
    const { onupdate, onremove } = watcher
    if (!path) return
    if (isEditorBackup(path)) return
    if (matches(path, ignore)) return

    try {
      const fullPath = join(root, path)
      const stat = await fs.lstat(fullPath)

      if (onupdate && stat.isDirectory()) {
        const paths = await fswalk(fullPath, ignore)
        for (const subPath of paths) {
          await onupdate(join(path, subPath))
        }
      }

      if (onupdate && extname(path)) {
        await onupdate(path)
      }
    } catch (error) {
      if (error.errno == -2 && onremove) {
        await onremove(path)
      }
    }
  })

  return watcher
}
When a file is deleted (errno -2), onremove broadcasts a removal event to the browser, which can then remove the corresponding element from the page.

Asset update pipeline

When watcher.onupdate fires, the serve function in cmd/serve.js routes the change through the asset graph:
// From cmd/serve.js
watcher.onupdate = async function(path) {
  const asset = await site.update(path)

  // site.yaml update — merge new config and broadcast
  if (asset.base == 'site.yaml') {
    const data = asset.content = await asset.parse()
    Object.assign(conf, data)
    return broadcast(asset)
  }

  if (asset) {
    asset.content = await asset.render({ hmr: true }) || await asset.text()
    if (asset.is_html) {
      asset.ast = await asset.parse()
      delete asset.ast.root
    }
    broadcast(asset)
  }
}
The hmr: true flag tells the render pipeline to produce the content fragment rather than the full page HTML. For a Markdown file, that means the article body. For a CSS file, that means the stylesheet text. The server calls broadcast(asset) to push it to all connected clients.

site.update() and cache invalidation

The site.update() method flushes the cached parsed content of the changed asset and (for new files) adds it to the asset registry:
// From site.js
async function update(path) {
  let asset = get(path)

  // update existing — flush cache so next render is fresh
  if (asset) { asset.flush(); return asset }

  // new file — create and register
  const file = await createFile(root, path)
  if (file) {
    files.push(file)
    asset = createAsset(file, site_opts)
    assets.push(asset)
    sortAssets(files)
    sortAssets(assets)
    return asset
  }
}
The flush() call clears the in-memory cache of parsed content (cachedText and cachedObj in file.js and asset.js), so the next call to asset.render() reads the file fresh from disk.

The development workflow with nue serve

Start the development server from your project root:
nue serve
By default this starts on port 4000. Use --port to change it:
nue serve --port 8080
1

Server starts

Nue reads site.yaml, walks the project tree to build the initial asset graph, starts the HTTP server and file watcher, and prints the local URL.
2

Open your browser

Navigate to http://localhost:4000. The page loads with a WebSocket connection already open for receiving hot reload updates.
3

Edit any file

Save a Markdown file, a CSS file, a Nue component, or a YAML data file. The watcher fires within milliseconds.
4

Browser updates

The server re-renders the changed asset and broadcasts it. The browser client applies the update — swapping stylesheet content, updating article HTML, or reinitializing a component — without a full page reload.

Contrast with framework-specific HMR

Webpack HMR works at the JavaScript module boundary. When a module changes, webpack rebuilds the module graph and pushes new module code to the browser, which attempts to apply it via the module.hot.accept API. CSS changes go through a style-loader that replaces <style> tags. Markdown and data files require custom loaders and are typically not hot-reloaded — they trigger a full page reload.The surface area is limited to what webpack knows how to bundle. If your content lives outside the bundle (a YAML file, a server route, a configuration file), it doesn’t participate in HMR.
Vite HMR is faster than webpack for JavaScript changes because it uses native ES modules — only the changed module and its importers are re-fetched. CSS hot-reload works well. However, like webpack, HMR coverage ends at the JavaScript/CSS boundary. Markdown content processed through Vite plugins may support HMR, but YAML data files, layout HTML, and server configuration changes still require manual page refreshes or full rebuilds.Framework-specific integrations (Vue, React, Svelte) add their own HMR logic on top of Vite’s base, which means different behavior across frameworks and additional dependencies.
Nue’s file watcher has no concept of a module graph because Nue does not bundle everything into JavaScript. Each file type is handled by its own render path. When any file changes — whether it’s a .css, .md, .yaml, .html, or .ts — the watcher fires the same onupdate callback, re-renders the asset, and broadcasts the result.The browser client is equally agnostic: it receives an asset object with a type and content, and applies the appropriate update (swap stylesheet, replace article HTML, reinitialize component). No module API, no framework hooks, no configuration required.

Server route hot reload

Server routes in @shared/server/ reload without restarting the server process. The createWorker function accepts a reload flag that triggers a fresh import() of the route module on each incoming request:
// From server/worker.js
export async function createWorker(opts = {}) {
  const { dir='@shared/server', reload } = opts
  await importWorker({ dir, reload })

  return async function(req) {
    if (reload) await importWorker({ dir, reload })
    // ... handle request
  }
}

export async function importWorker({ dir, reload }) {
  const path = join(process.cwd(), dir, 'index.js') +
    (reload ? '?t=' + Date.now() : '')
  routes.length = 0
  await import(path)
}
The cache-busting query string (?t=timestamp) forces Bun’s module loader to treat each import as a new module, bypassing the module cache. This gives you live-reloading API handlers during development without any manual intervention.
Server route reload is development-only. In production (nue build), routes are loaded once at startup for maximum performance.

Build docs developers (and LLMs) love