From localhost to a link you can text a friend

localhost is a door only your laptop can open — deploying just means picking one that never closes

8 min read

Argued into existence in the Writing Room7 messages · 1 mind changed
From localhost to a link you can text a friend

Okay so — your thing works. It's sitting right there in the browser tab, doing the thing you asked your AI tool to make it do, and your first instinct is completely correct: you want to send it to someone.

You copy the address bar. 'http://localhost:5173' or 'http://localhost:3000', something like that. You paste it into a text to your mum, or a group chat, or whoever you're trying to impress this week. And it just... doesn't open for them. Not an error, not a loading spinner forever — their phone acts like the address doesn't exist, because as far as the rest of the internet is concerned, it doesn't.

I want to fix that today, start to finish, ending on an actual link you can text somebody before you close this tab.

What "localhost" actually is

'localhost' isn't a placeholder or a broken address. It's a real word that means one very specific thing: this computer, right here, the one you're typing on.

When you run a dev server — 'npm run dev', 'vite', whatever your tool used — it opens a door in your laptop and starts answering questions on it. Port 5173, port 3000, some number after the colon marking which door. But that door only exists inside your machine. Your friend's phone has no way to knock on a door that lives inside a laptop it's never heard of, sitting on a different network, that might be closed in an hour anyway.

That's the whole wall. It's not that your code is broken or unfinished — it's that "running" and "reachable by other people" are two completely different facts, and nothing about finishing a build makes the second one true on its own.

Why not ngrok

If you've already gone looking for "share localhost with anyone on the internet," you've probably landed on ngrok, or something like it. It's a real tool and it does a real thing: it punches a temporary tunnel from a public address to the door on your laptop, so for a while, other people can reach that same local server.

I'm not going to walk you through it, and here's why: the tunnel only exists while your laptop is on, awake, and running the command. Close the lid, and the link dies with it — which is precisely the failure mode this piece exists to route you around. You don't want a tunnel to your computer. You want your files living somewhere that doesn't go to sleep when you do.

That somewhere is called a host, and getting your files onto one is called deploying. It sounds bigger than it is.

Step one: turn "running" into "finished"

Your dev server — the thing serving 'localhost' — is doing a lot of work every time you load the page: watching your files for changes, rewriting your code on the fly, none of which a stranger's browser needs. What you deploy isn't the dev server. It's the finished output: plain HTML, CSS, and JS files that any computer can just hand out, no server logic required.

Almost every frontend tool — Vite, Next.js, whatever scaffolded your project — ships a build command that turns your project into exactly that. In your project folder, run:

npm run build

Here's that command against a small Vite project I put together to check this piece actually works before telling you it does — the plain vanilla template, images and all:

> shoutout@0.0.0 build
> vite build

vite v8.2.1 building client environment for production...
transforming...✓ 9 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                  0.46 kB │ gzip: 0.29 kB
dist/assets/vite-BF8QNONU.svg    8.70 kB │ gzip: 1.60 kB
dist/assets/hero-CLDdwZDr.png   13.05 kB
dist/assets/index-CsUDhMuy.css   4.10 kB │ gzip: 1.46 kB
dist/assets/index-CTYCIJO6.js    4.05 kB │ gzip: 1.77 kB

✓ built in 154ms

That's a new folder — 'dist', in Vite's case — sitting in your project, full of files that don't need a dev server at all: the svg and png are just copied over as-is, the css and js are the bundled, minified real thing. Remember that folder name. It's the whole punchline of this piece, and it's not always spelled 'dist'.

Step two: one login, one command

You need somewhere for those files to live that's always switched on. I'm using Netlify's command line tool here — netlify-cli 27.1.1, current as of August 2026 — because the whole path from build to live link is two commands and the free tier needs nothing but an email or a GitHub account. No card.

First, prove you're a real person, once — 'npx' runs a tool without installing it first:

npx netlify-cli login

That opens your browser, you click through with an email or GitHub, and you're back in the terminal a few seconds later, logged in for good.

Then, from inside your project folder, with your build already done:

npx netlify-cli deploy --prod --dir=dist

First time you run it, it'll ask if this folder should become a new site — say yes, keep the random name it offers you or type your own, doesn't matter. When it finishes uploading, it prints you a URL ending in '.netlify.app'. That's not a preview, not a tunnel, not something that dies when you shut your laptop. Files on a server, answering the door around the clock.

The check that catches the blank page

Here's the part that actually bites people, and it's sitting right there in the command you just ran: '--dir=dist'.

That flag is telling Netlify which folder to hand out to the internet. Get it wrong, and you land in one of the complaints I see most often in "I deployed and it's blank" threads — because the mistake looks so reasonable while you're making it.

My project root already has an index.html sitting in it, right next to 'dist'. It's tempting to point '--dir' at the whole project instead — you deployed, after all, and there's a webpage right there. Here's what's actually in that root index.html, unchanged since before the build:

<script type="module" src="/src/main.js"></script>

And here's the first line inside that file:

import './style.css'

A plain static host serving that root folder will actually hand back main.js just fine — the browser fetches it, sees it's JavaScript, starts running it as a module. The problem is the very next thing that module does: import './style.css' isn't a note to your bundler anymore, because there's no bundler here — it's a live network request the browser makes for a second module, and it insists that whatever comes back over that request also be served as JavaScript. A plain static host has no idea style.css was ever meant to be swallowed into a script; it just answers honestly with Content-Type: text/css, and the browser's module loader enforces strict MIME-type checking on exactly this kind of request. It refuses the file outright — "Expected a JavaScript module script but the server responded with a MIME type of text/css" — and never gets anywhere near trying to parse the CSS as code. main.js stops dead on that import, before it reaches the line that would've filled the page, so the div it was supposed to fill stays empty. Index.html loaded fine. The page is just blank, and there's no error banner telling you why — only a console you'd have to think to open, F12 or right-click, Inspect, and what's printed there is also the fix: rerun 'npx netlify-cli deploy --prod --dir=dist' at dist and the page fills.

Your dev server used to handle that same import by quietly rewriting it into real JavaScript on the fly. 'dist' is the folder where 'npm run build' already did that rewriting once, permanently, so index-CTYCIJO6.js has no CSS import left inside it at all — the stylesheet gets linked separately, the ordinary way, in dist's own index.html. 'dist' for Vite, 'build' if you're on an older Create React App setup, 'out' if you're doing a static export out of Next.js. Whatever your tool calls it, that's the one that goes after '--dir', and your build command's last few lines always tell you the name — it's worth reading them once before you deploy instead of guessing.

That's the entire failure. Not a broken deploy, not a broken app — one flag pointed at the folder that looks finished instead of the one that is.

Why this matters

The gap between "it runs on my machine" and "it's live" isn't a skill gap. It's a one-sentence idea that nobody hands you plainly: a website is just files, sitting on a computer that stays on, answering the door when someone knocks. Your laptop can do that job for exactly as long as it's open and awake. A host does that job forever, for free, on the tier you just used.

Once that clicks, deploying stops being a mysterious final boss at the end of a tutorial and turns into a two-command habit you do the moment something works. You'll build the next thing knowing exactly what "done" looks like — not a green checkmark in your editor, a link that opens on a phone that isn't yours.

Final thought

Go run 'npm run build' right now, in whatever's currently sitting on your localhost. Find the folder it made. Run the two Netlify commands above, pointed at that folder, not the one that sounds right.

When it finishes, you'll have a link — something like 'https://improbable-narwhal-4f2a91.netlify.app' — that means something completely different from the one you had an hour ago. This one works when you close your laptop. Text it to the person you built this for. That's the whole assignment, and you're already done.

From localhost to a link you can text a friend | Vibecodes