hosting

Deploy a Vite dist folder without Git

By PreviewSend Team · Jul 19, 2026 · 8 min read

You have a Vite app (React, Vue, Svelte, or plain) and want a link to send someone. Not a repository or a deploy pipeline, just a hosted URL. The path is short: build the project so Vite writes a dist folder, confirm the asset paths are right, preview it locally, then zip the contents of dist and upload that one archive.

Concretely, that's four commands:

npm run build       # compiles your source into ./dist
npm run preview     # serves dist at http://localhost:4173 so you can check it
cd dist
zip -r ../site.zip .

Then upload site.zip and you have a URL. The rest of this guide is the detail that keeps each step from going wrong: which folder is actually the output, why assets sometimes break after upload, and when a Vite app can't be hosted this way at all.

Find your package manager and build script

Before anything builds, know which tool runs it. The lockfile in the project root is the tell:

  • package-lock.json → npm
  • pnpm-lock.yaml → pnpm
  • yarn.lock → yarn
  • bun.lockb → bun

Open package.json and look at the scripts block. A Vite project's build script almost always runs vite build:

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
}

Run that script with the tool the lockfile points to: npm run build, pnpm build, yarn build, or bun run build. Vite's static deploy guide assumes this setup: a local Vite dependency with build and preview scripts, using the equivalent command for your package manager.

Run the build and find dist

npm run build compiles your source into static HTML and hashed asset bundles. By default Vite writes them to a dist folder, and its production build docs describe this as the app packaged for deployment. A finished dist looks roughly like this:

dist/
├── index.html
└── assets/
    ├── index-a1b2c3.js
    └── index-d4e5f6.css

That index.html is the transformed entry page a static host serves at /. A standard Vite source tree already contains index.html; the build rewrites its module and asset references and emits the deployable copy into dist.

Check these before you go further:

  • Confirm the output folder. dist is the default, but a project can override it. If vite.config.js (or .ts) sets build.outDir, the compiled files land somewhere else, perhaps build/. Use whatever that config names.
  • Don't zip the source by mistake. If your archive ends up containing package.json, src/, and node_modules but no top-level index.html, you zipped the project, not the build. That's the single most common upload failure, and fixing an "index.html not found" error walks through spotting it.

Check the base path before you build

This matters when the host mounts your site under a path instead of at the domain root. PreviewSend's production preview URLs are root-mounted, so Vite's default works there. Custom domains are root-mounted too, but require a purchased Starter, Pro, or Agency plan and are not included in the Pro trial. A different host might publish the same files at example.com/demo/, where the setting needs attention.

Vite's base option is the public path your assets are served from, and it defaults to /. With that default, the built index.html references assets with a leading slash:

<script type="module" src="/assets/index-a1b2c3.js"></script>

A leading slash is a root-absolute path. Per MDN's reference on resolving relative references, a path beginning with / resolves against the site root, ignoring where the current page sits. That's fine when your site owns the domain root. Serve the same files under a subpath and the browser looks for /assets/… at the wrong place, producing a 404 for each bundle.

If you do not know where an archive will eventually be mounted, the portable option is to make its URLs relative. Set base to ./ in your config, the value Vite documents for embedded deployment, and rebuild:

// vite.config.js
export default {
  base: './',
}

Now the same tag reads src="./assets/index-a1b2c3.js", which resolves next to the page wherever it's served. Change base, then run the build again so the new paths are written into dist.

PreviewSend's production preview URLs and custom domains serve each site at the domain root, where Vite's default base: '/' is correct. PreviewSend also injects a <base href> pointing at the document's real directory (src/lib/preview/base-href.ts). Relative references from a base: './' build therefore keep working on nested SPA routes. Root-relative paths still resolve from the visitor's domain root.

Preview the build locally

Don't upload a build you haven't looked at. npm run preview (which runs vite preview) serves the contents of dist locally, normally at http://localhost:4173:

npm run preview

Open that URL and click through the app. This serves the exact files you're about to zip at a local domain root. Open the browser's Network tab, reload, and watch for failed asset requests. It catches broken bundles and missing files, but it does not simulate every possible subpath. If the destination uses one, check the generated asset URLs against that path too.

This is a check, not the deployment. Vite says preview is meant for local inspection, not production hosting. It runs on your machine and stops when you close the terminal. The hosted copy comes from the upload, not from vite preview.

If your app uses client-side routing

A Vite single-page app with a router, such as React Router or Vue Router, renders its routes in the browser. Visit / and everything works, but refresh a deep link like /settings and a plain static host looks for a file at that path, finds none, and returns a 404. The page only ever existed as a client-side route.

Static hosts handle this by falling back to index.html for routes, letting the router take over. PreviewSend does this automatically for extensionless paths: when a route has no matching file or directory index, it serves the version's entry index.html. Missing asset-like paths with file extensions still return 404. A routed Vite build therefore works without extra redirect rules on your end, while a host without an SPA fallback will 404 on deep links.

Zip the contents of dist, not the folder

Package the build so index.html sits at the top of the archive, not inside a dist/ wrapper:

cd dist
zip -r ../site.zip .

The trailing . compresses the folder's contents rather than the folder itself, so index.html lands at the archive root. Without the terminal, open dist, select everything inside it, and compress the selection. Zipping the enclosing dist folder buries the entry file one level down, which many hosts reject; the ZIP hosting guide and the plain HTML, CSS, and JavaScript folder walkthrough both cover that layout in more depth.

Upload it and update the same URL later

With site.zip holding an index.html entry, the upload is the easy part. Start a free PreviewSend workspace and upload the archive (up to 100 MB compressed). PreviewSend checks the ZIP's size and safety limits, selects an index.html, extracts the files, makes that version active, and returns a stable URL. It does not execute the project or prove that the build has no backend dependency. Eligible first-time owners get a one-time 14-day Pro trial on their first workspace without entering a card. Afterward, Free limits apply: the most recently updated site stays live and excess sites are archived.

The URL is the reason this beats re-sending a ZIP. When you change the app, rebuild, re-zip dist, and upload again. The new build becomes the active version at the same address, and earlier versions stay on file up to your plan's retention limit. Everyone holding the link sees the current build without a new attachment. The file-level version diff is available on Pro, Agency, and during the active Pro trial.

New projects also enable client review by default. A client can pin comments or request changes on the page without creating an account, and each note stays tied to the version they viewed. You can export the version's comments as CSV or Markdown, or turn unresolved feedback into an AI-ready revision plan before rebuilding the next dist.

You can add a password or an expiry date to the project too. Normal hosted-site responses and password challenges carry an X-Robots-Tag: noindex, nofollow header, which asks search engines not to index the page. That is not access control; anyone with the link can still open the site. The password is the actual gate. Turn it on and the URL serves a challenge before it serves your files.

When this approach doesn't apply

This works because a built Vite frontend is just files: the host returns them, the browser runs the app. It stops working the moment something has to run on the server for each request.

That's the case for server-side rendering, whether it is a Vite app with an SSR entry or a meta-framework running in server mode. It also applies when an API renders responses per request from the same process. There, dist isn't the whole app; there's a server bundle that a file host has no way to execute. A static frontend can still talk to a separately hosted API over the network, but the API itself can't live in the ZIP.

The quick test: if npm run build produces only a folder of static files and npm run preview shows the entire app working, you're static and this guide fits. If the build also emits a server bundle (an entry-server file, a Node handler), that part needs a runtime, not a file host. For a framework-agnostic version of this decision, what a ZIP host can and can't serve works through the same static-versus-dynamic split for Next.js, Astro, and AI-builder exports too.

FAQ

What is the difference between my Vite project folder and the dist folder?

The project folder is source: package.json, a src/ directory of .jsx/.vue/.svelte files, and node_modules. Vite compiles that into a dist folder of plain HTML and bundled, hashed JavaScript when you run the build. The host serves dist; it never sees or runs your source. Zipping the project folder instead of dist is the usual reason an upload has no index.html.

My styles and scripts 404 after I upload, but the build previewed fine. Why?

Check where the host mounts the site. Vite defaults base to /, which is correct at a domain root. If the site lives under a path such as example.com/demo/, root-relative assets request example.com/assets/... instead. Set base to the known subpath, or use ./ for a portable archive, then rebuild and re-zip.

Can I use pnpm, yarn, or bun instead of npm?

Yes. The lockfile tells you which one the project uses: pnpm-lock.yaml means pnpm, yarn.lock means yarn, bun.lock or the older bun.lockb means Bun, and package-lock.json means npm. Run the build script with that tool (pnpm build, yarn build, or bun run build). The dist folder serves the same purpose.

Is vite preview a way to host the site?

No. vite preview serves the built dist folder locally, normally at http://localhost:4173, so you can check the production build before shipping it. Vite is explicit that it is not meant to be a production server. Use it as a final verification step, then upload the same dist to a real host.

Have a site to host? Upload it and get a live link in seconds.

Get it free

← Back to the blog