Serving an HTML file locally

This is the right first move when a report renders blank, and the wrong last move when someone else needs to read it. Both halves matter.

The commands

From the directory that contains index.html:

python3 -m http.server 8000          # stdlib, nothing to install
npx serve .                          # Node
npx http-server -p 8000              # Node, older and still fine
php -S localhost:8000                # if PHP is what you have

Open http://localhost:8000. If there's no index.html in that directory you'll get a file listing instead of the report — that's the server telling you that you're one level too high.

Why this fixes the blank page

A file opened by double-clicking has a file:// URL, and browsers give those an opaque origin. Under the same-origin rules that means:

  • fetch() and XMLHttpRequest to a neighbouring file are blocked.
  • <script type="module"> won't load — module fetches follow CORS even for local files.

Any report that ships its results as a separate JSON file and loads them at render time is structurally unopenable that way. Over http:// the same requests are same-origin and go through untouched. Longer diagnostic list in why my HTML report lost its CSS.

What it does not fix

Nobody else can open it. localhost is their machine. Sending the URL sends them to a connection-refused page.

Your LAN is not the reviewer's LAN. python -m http.server binds 0.0.0.0 by default, so a colleague at the same office can reach http://192.168.x.x:8000 while it's running. Add --bind 127.0.0.1 if you'd rather it didn't. Either way it's useless for anyone remote, on a phone, or reading it tomorrow.

There's no HTTPS, no auth, and no lifetime. The stdlib server has no access control of any kind, and the whole thing ends when you press Ctrl-C or the laptop sleeps.

The tunnel option, and its real cost

cloudflared tunnel --url http://localhost:8000
ngrok http 8000

Both hand you a public hostname in a few seconds. Reasonable for a demo you are actively on a call for. For a report someone opens later:

  • The URL dies with the process — and it's a different URL next time.
  • Free tiers insert an interstitial page and rotate the hostname.
  • Access control is "the hostname is hard to guess", which is not access control. Anyone the link reaches, and anything that scrapes it, is in.
  • You've published a port on your working machine to the internet for the duration.

When to just publish it

If the answer to "who reads this, and when" is anyone other than you, right now, the local server has done its job and should be replaced by a URL. Drag the directory (or a zip of it) into Comma:

  • index.html becomes the report body and the sibling assets upload with it — relative references rewritten to the uploaded copies, which is the same problem the local server was solving, solved permanently.
  • Scripts still run, sandboxed, so the interactive parts stay interactive — interactive HTML reports.
  • Access is per report: private, team, anyone signed in, or link holders with view, comment or edit. See sharing & access control.
  • Comments anchor to the content, so the reply comes back on the report rather than in a thread three days later — commenting on HTML.

For the CI version of the same move — generate, publish, keep one URL across runs — see publishing from CI.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history.

Publish instead of serving →

Related