live-server.nvim

Start it, forget it, keep the same URL.

A development server manager for Neovim — static sites and your Next.js, Vite or Express app alike. Start and stop servers, watch their output, pin a port to a project so its URL never changes, and get sensible behaviour when Neovim is running over SSH.

Three papercuts, fixed

The usual live-reload workflow works right up until it doesn’t.

The URL kept moving

Ports are pinned per project. Bookmarks, an open devtools session, a CORS allowlist and a QR code taped to a test phone all keep working across restarts and across machines.

“Running” didn’t mean reachable

A browser opens only once the port actually accepts connections — not after a fixed one-second guess. A server that spawns but never answers is reported as such, not shown as healthy.

SSH quietly broke it

Editing on a remote box means the server binds loopback over there. The plugin detects it, hands you the exact ssh -L command, and copies the URL to your clipboard over OSC 52.

Install

Neovim 0.9+ and at least one backend. The plugin finds whichever you have.

{
  "G00380316/live-server.nvim",
  cmd = "LiveServer",
  keys = {
    { "<leader>ls", "<Cmd>LiveServer toggle<CR>", desc = "Toggle live server" },
    { "<leader>lm", "<Cmd>LiveServer<CR>",        desc = "Live server manager" },
    { "<leader>lo", "<Cmd>LiveServer open<CR>",   desc = "Open in browser" },
  },
  opts = {},
}

Backends

BackendInstall Live reloadNotes
nodenothing extraPer frameworkThe project’s own dev server. Picked automatically inside a framework project.
live_servernpm i -g live-serverYesInjects CSS without a page reload.
browser_syncnpm i -g browser-syncYesSyncs scroll, clicks and input across devices.
servenpm i -g serve or npxNoClean URLs, correct MIME types, production-like.
pythonalready installedNoZero-install fallback.

opts = {} is enough — every option has a working default, and the plugin configures itself on first use if you never call setup() at all. Run :checkhealth live_server to see what it found.

Commands

One command with subcommands and real Tab completion. A bare port or backend name is treated as a start request.

CommandDoes
:LiveServerOpen the manager
:LiveServer toggleStart this project’s server, or stop it
:LiveServer start 3000Start on a specific port
:LiveServer start browser_sync dir=docsPick a backend and a directory
:LiveServer restartRestart on the same port
:LiveServer openOpen the current file’s page
:LiveServer logsTail the server’s output
:LiveServer pin 4000This project always uses port 4000
:LiveServer exposeLet other devices reach it (asks first)
:LiveServer reapStop servers left by a crashed session

In the manager

KeyAction KeyAction
Enter oOpen in browserdStop and remove
sStart / stopeToggle network exposure
rRestart, same portfSSH forwarding command
pChange portXStop everything
PPin / unpin portTabNext server
lShow output?Help

Every one of these is also a :LiveServer subcommand — nothing is keyboard-only.

Next.js, Vite, Express & friends

In a framework project, :LiveServer toggle runs the dev server the repository already defines — rather than serving your source tree as files, which would just render a page of raw JSX.

:LiveServer toggle    # -> pnpm run dev --port 3000 --host 127.0.0.1

Detected, not configured

The framework comes from package.json dependencies — most specific first, so SvelteKit is never mistaken for the Vite it is built on — and the package manager from the lockfile.

The port arrives correctly

Next.js gets --hostname, others get --host, Vite gets --strictPort so it fails instead of drifting. Express and CRA get PORT in the environment, the convention they share.

It believes the process

Hard-coded app.listen(4000)? The plugin reads the address the server announces, adopts that port and says so. A working server is never reported as unreachable because of our bookkeeping.

It asks before it runs anything

Serving files reads files. Starting a dev server executes code the repository wrote, so the first time it shows you exactly what that is.

Run the Next.js dev server defined by this repository?

  script:  dev = next dev
  via:     pnpm

Defined in ~/code/shop/package.json. This executes code from the repository.

The answer is remembered against that script, not the whole file — a dependency bump won’t ask again, but changing what dev runs will. Recognised frameworks also get a longer startup budget, because a cold Next.js compile can take a minute before the port answers.

Ports that stay put

By default the port a project ends up with is remembered and reused. Availability is checked against the operating system, so a port held by a stray Docker publish or another editor is correctly seen as taken.

StrategyBehaviour
"pin"Reuse the project’s remembered port. Default.
"scan"First free port in port.range.
"stable"Derived from the project path — same checkout, same port on every machine, no shared state.
"fixed"Always the configured port; fail rather than move.

Per-project configuration

Drop a .liveserverrc.json in the project root and everyone on the team gets the same setup.

{
  "server": "live_server",
  "port": 4321,
  "root": "public",
  "open": "index.html",
  "entry_file": "index.html"
}

Over SSH

When SSH_CONNECTION or SSH_TTY is set, the plugin stops pretending a browser will open and prints what you can actually act on.

Serving portfolio at http://127.0.0.1:5500/ (on build-box)

Forward it from your machine:
  ssh -N -L 5500:127.0.0.1:5500 you@build-box

URL copied to your clipboard via OSC 52.

OSC 52 means the URL lands on your local clipboard even when the remote machine has no clipboard tool at all.

Security

A dev server serves your working tree, including files you have not committed. Two decisions follow from that.

Loopback by default

Servers bind 127.0.0.1. Exposing one to the network is possible and sometimes necessary — testing on a phone — but it is always a deliberate, confirmed act.

No accidental code execution

Nothing a repository defines — project-file command fields or package.json dev scripts — runs without consent, recorded against the exact command. Any edit revokes it and asks again, showing what it wants to run.

No shell, ever

Processes are spawned with argv lists, so a directory name can never become a command. Project files cannot serve anything outside their own project, and privileged ports are rejected.

Configuration

Unknown keys, wrong types and out-of-range values are reported in one message and then ignored — a typo never breaks your init.lua.

require("live_server").setup({
  server = "auto",            -- or "live_server" | "browser_sync" | "serve" | "python"
  host = "127.0.0.1",
  expose = "ask",             -- true | false | "ask"

  port = {
    strategy = "pin",         -- "pin" | "scan" | "stable" | "fixed"
    range = { 5500, 5599 },
  },

  browser = {
    auto_open = true,
    open_current_file = true, -- open the page for the buffer you are in
  },

  ui = {
    icons = "auto",           -- "auto" | "nerd" | "ascii" | "text"
    spinner = true,           -- false removes all motion
  },
})

Accessibility

Status is always spelled out as a word next to the icon — nothing is conveyed by colour or glyph alone. ui.icons = "text" removes glyphs entirely, "ascii" avoids non-ASCII, and ui.spinner = false removes all motion. Highlight groups are default links to standard groups, so the panel follows your colorscheme’s contrast in light and dark. On a narrow terminal the panel opens as a split rather than a cramped float, and lines never exceed the window width.

Full reference: :help live-server. Everything from the previous version still works — old commands and modules forward to the new API and warn once. See :help live-server-migration.