miinideckmiinideck
PricingUse casesBlog
Sign in
How-to & formats

Host an Angular build as a link: what works, and where deep links break

You ran ng build and now someone needs to open it — QA, a client, a stakeholder — before the real deployment gets scheduled. What to upload, what happens to relative asset paths, and the one honest limit: a refresh on an in-app route returns 404 unless you switch to hash routing.

By miinideck·September 9, 2026·10 min read

Your ng build finished. Someone now needs to open the result — QA on their own machine, a client on their phone, a stakeholder who is going to click three things and send one comment back — and the actual deployment is two approvals and a change window away.

That gap is a real, ordinary problem, and it does not require a deployment to solve. What it requires is knowing which folder to hand over, and one honest limitation about routes.

TL;DR
  • Upload the build output, not the project. A browser cannot run .ts — the compiled folder is the deliverable.
  • Zip that folder and drop it. The wrapper folder is stripped automatically, so relative asset paths resolve the way they did locally.
  • The honest limit: a refresh on an in-app route returns 404. There is no index.html fallback here — the bundle is served as the files it contains. The entry link works; navigating inside the app works; a pasted deep link does not.
  • Two ways round it: switch the review build to hash routing, or just send the entry URL and let people navigate from inside.
  • If the app needs SSR, an API route or a database, this is the wrong tool — Vercel and Netlify are built for that and it is worth going there early.

What do I actually upload — the project or the build?

The build.

This trips people up often enough that the failure is worth describing precisely, because it is the difference between a clear error and a link that opens to a white screen.

ng build compiles your TypeScript, your templates and your styles into plain files a browser can execute: JavaScript, CSS, an index.html with the right script tags injected, plus whatever you had in your assets folder. That output folder is the whole deliverable. The project folder around it — src/, node_modules/, your config — is build input, and none of it means anything to a browser.

Where that folder is depends on your Angular version, and this is the part worth getting exactly right. ng build writes to dist/<project>/ by default — but under the current @angular/build:application builder that path is only the base. The part a browser can actually serve sits one level down. Angular's own workspace-configuration reference describes that key as "The output directory name for your browser build is within the base output path. This can be safely served to users", and gives its default name as browser.

So before you zip: open dist/<project>/. If there is a browser/ folder inside it, that inner folder is the deliverable — not the one above it. Zipping the wrong level gives you a link that opens to a directory of folders instead of your app. (Both defaults read from Angular's official docs on 2026-09-10; outputPath can be overridden in angular.json, so check yours if the build was customised.)

Upload the project by mistake and you get told, rather than getting a broken link: a ZIP that still contains source files is rejected as an unbuilt project, with the instruction to upload the build folder instead. That behaviour is deliberate — guessing which HTML in a source tree is "the build" is a heuristic, and a wrong guess produces a successful-looking link that opens to nothing, which costs more trust than a plain error does.

How do I get the link?

Four steps, and the third is the one people skip.

  1. Run the production build. Whatever your team's build command is — the output folder is what matters, not how you got it.
  2. Zip the build output folder itself. Select the folder, compress it. Do not zip the repository root with the build folder somewhere inside it.
  3. Check the entry file is at or near the top. The entry point is picked as the root index.html if there is one, and otherwise the shallowest HTML file in the bundle. A normal Angular build gives you exactly one candidate, so this is usually automatic.
  4. Drop the ZIP and copy the link. The link is unguessable and carries a noindex by default, so the review build does not turn into something a search engine finds.

One thing worth knowing because it silently saves you: a single wrapper folder is stripped on the way in. Most "compress this folder" actions produce a ZIP whose entries all begin mybuild/…. Without stripping that, every relative asset path in your index.html would resolve one directory too high and every stylesheet would 404. The stripping only happens when every entry shares the same first segment, which is exactly the wrapper-folder case and nothing else.

If you want the wider picture on bundles rather than the Angular-specific path, the best free way to host a ZIP bundle covers the same ground across hosts.

Why does refreshing on /dashboard give me a 404?

Because that URL looks like a file path and there is no file there.

A router that uses the browser History API rewrites the address bar as the user navigates — /dashboard, /orders/1182 — without ever asking the server for those paths. The first load of the app came from your entry HTML, and everything after that happened in JavaScript. So the URLs are real to the app and imaginary to the host.

Hosts that support this do it with a rewrite rule: any path that does not match a file returns index.html, and the app reads the URL and renders the right view. That rule is not applied here. A request for a path that is not in your bundle returns 404, full stop — the same behaviour whether the missing file is /dashboard or a mistyped image name.

Practically, that means:

What the reviewer doesWhat happens
Opens the link you sentWorks — that is the entry file
Clicks around inside the appWorks — no server request involved
Refreshes while on an in-app route404
Pastes an in-app URL to a colleague404 for the colleague

So the limitation is narrow and it has a shape: it bites the moment a URL travels on its own.

Should I switch to hash routing, or just send the entry link?

Depends on who is reviewing and how they work.

Send the entry link when the review is one person opening one link and walking through the flow. Add a line to your message — "start from here, the app handles navigation" — and the 404 case never comes up. This is most reviews.

Switch the review build to hash routing when the URLs are going to get passed around: a QA team filing tickets with reproduction links, a client emailing "look at this screen" to a colleague, anything where someone will copy the address bar. With hash routing every route lives after a #, the browser only ever requests the entry file, and refreshes and pasted links both work.

The current way to turn that on is withHashLocation(), passed to provideRouter alongside your routes:

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(appRoutes, withHashLocation())
  ]
});

withHashLocation comes from @angular/router and is marked stable in Angular's API reference (read 2026-09-10). Keep it to the review build if your production deployment serves real paths — it changes every URL your reviewers see.

That trade — one file that renders from anywhere versus real paths that need a server — is the same one that shows up when you ask an AI tool for a multi-page bundle; prompting for a multi-page site that still ships as one bundle walks the routing half of it in more detail.

If you have a build output folder sitting on your machine right now, zipping it and dropping it takes about as long as reading this section. The No-account path takes a file up to 3 MB with no signup at all; a free account raises that and adds the password and expiry controls below.

Try it with your build

What if the app has SSR or an API route?

Then this is the wrong tool for that half of it, and it is much cheaper to know that now.

A static bundle is files. It can call an API that already exists somewhere reachable from a browser — that works fine, and a review build pointed at a staging API is a completely normal setup. What it cannot do is be the server: no server-side rendering, no API routes of your own, no environment variables, nowhere for a secret to live.

Angular makes that an explicit build setting, which is useful here because it means you can check rather than guess. ng build takes an --output-mode flag (outputMode in angular.json) with two allowed values: Angular's CLI reference defines static as "Generates a static site build artifact for deployment on any static hosting service" and server as "Generates a server application build artifact, required for applications using hybrid rendering or APIs" (read 2026-09-10). A static build is the one that becomes a link. A server build expects Node to be running somewhere.

When the server half is what you need, go and get a platform that provides one. Vercel and Netlify both take a repository and give you server routes, environment variables and a secrets store. That is a genuinely different product from a page host, this one included, and picking it at the start is far cheaper than discovering the need halfway through a review cycle. The wider version of that call is in private link vs public deploy.

Can I password it, expire it, or replace the file later?

All three, once the upload is attached to an account — and they are the reason this shape suits a review build rather than a deployment.

Password — free on every plan, including the free one. Worth it whenever the build shows real data or an unreleased feature. (An upload made with no account has no password or expiry controls at all, because there is no account for those settings to attach to.)

Expiry — free links run on a 7-day clock by default, which happens to match the natural life of a review round. You can set your own date, and on the free tier you can keep one always-on link that never expires.

Replacing the build — upload a new version behind the same URL. The people you sent it to keep the link they already have, which is the difference between "here is build 4" and "here is the review link" as a message you send once.

The per-file ceiling on the free tier is 10 MB, which a normal Angular production bundle sits comfortably under; the full ladder is in when does free HTML hosting stop being free.

What this isn't

This is a way to put a finished build in front of a person. It is not a deployment target, and it should not become one: there is no build pipeline, no branch previews wired to your repo, no rollback history, no custom headers or redirect rules. If you find yourself wanting those, you want the real platform, and the fact that the review link was easy is not an argument for stretching it.

What it is good at is the gap the release process leaves open — the two days between the build is ready and the build is deployed, when somebody still needs to look at it.

More in How-to & formats

How do I edit a password-protected HTML page without re-encrypting it?

Client-side encryption tools turn your page into an encrypted file, so the protection and the artifact are the same object — every edit means re-running the tool and re-uploading. What that actually costs, the salt setting that decides whether your old share links survive, and when a hosted password is the better trade.

September 25, 2026·6 min read

Export a Slidev or reveal.js deck to a private link (2026)

Your Slidev or reveal.js deck is already a self-contained web app. Skip the public deploy, the screenshot, and the PDF flattening: run the build, zip the dist folder, and turn it into one private link that keeps every fragment, transition, and code highlight stepping exactly as it did locally.

September 11, 2026·6 min read

You already sent the link. Now the file changed. (2026)

Replacing a page you have already handed someone is a different problem from publishing it the first time. Why the usual answer is 'learn git', who that answer is actually for, and what the other routes really cost.

August 23, 2026·7 min read

Send your own private link.

miinideck turns a single HTML file into an unguessable link with optional password and expiry. Default-private, never indexed.

Try it free →See pricing
miinideck

HTML files, finally as links — for AI builders, agencies, and consultants. Default-noindex, default-private, default-yours.

Product

  • Pricing
  • Use cases
  • Try it free

Resources

  • Blog
  • Free tools
  • Featured on
  • Report abuse

Legal

  • Privacy
  • Terms
Listed onmiinideck listed on Product Huntmiinideck listed on Faziermiinideck listed on TheSaaSDirmiinideck listed on AIToolHuntmiinideck listed on LaunchNest
© 2026 miinideckMade for people who don't want their work indexed.