# Deploy a React and Vite site to production

Build your React/Vite frontend locally, inspect the files that will become public, and publish the output with the Shipvela CLI. This walkthrough includes a runnable example, an actual local build check, and a way to distinguish a failed build, missing asset and broken client-side route.

Content updated: 2026-09-30

## Choose the right deployment path before building

This walkthrough uses a static upload. The browser receives HTML, CSS and JavaScript; it does not start your development server. You can use your own app or the small downloadable example below. First check whether the app needs a server process as well as a frontend.

Which deployment path fits your project?

| Project you have | Path to use | What to check |
| --- | --- | --- |
| React/Vite browser app with a dist folder | Build locally, then `shipvela publish dist --spa` for a new upload project. | Private API operations must already live in a separate backend. |
| Supported npm React/Vite repository at its root | Use the [manual GitHub guide](https://shipvela.com/guides/manual-github-deployment). | Commit/push first, then explicitly deploy. A push alone does not publish. |
| Astro static output or a Next.js static export | Publish the already-built static folder; Next.js exports normally use `out`. | Use `--spa` only for a client-side single-page app, not merely because a tool generated the site. |
| An app that starts a general Node server or non-Next SSR runtime | Keep a suitable server host or deliberately produce a genuinely static export. | A folder named dist is not proof that an application is static. |
| Compatible Next.js 12–15 SSR | Separate GitHub deployment path on a paid plan. | This static-upload guide does not enable SSR, databases or a backend server. |

![Source and public environment values are built into dist, uploaded through Shipvela validation, then served as public files to the browser.](https://shipvela.com/guide-assets/react-vite-deployment.svg)

Original workflow diagram. The build runs locally; the later Shipvela job and public URL must be verified separately.

## Prepare the project, runtime and destination

- Use Node.js 22.12+ on a supported release line and npm. CLI 0.3.0 requires Node 22+; this example was compiled with Node 22.20.0, Vite 8.3.1 and React/React DOM 19.3.0. Check your own app’s engine requirements before switching runtimes.
- Have a Shipvela account and install its CLI from [the versioned setup instructions](https://shipvela.com/docs#cli). Do not substitute an unverified npm package with a similar name.
- Identify the app’s build script and output directory. Review any existing `shipvela.json`: it determines which project a subsequent publish updates.
- Configure only public frontend values before building. A private API key must not be placed in a `VITE_` value. Read [the environment variable guide](https://shipvela.com/guides/vite-production-environment-variables) if the app needs configuration.

Check the tools in the terminal you will use

```sh
node --version
npm --version
shipvela --version
```

The first two commands report installed runtime versions; the copied CLI checked for this guide reports `0.3.0`. A successful version check does not log you in or prove that hosting is configured. Current Vite requirements are documented in [Vite’s getting-started guide](https://vite.dev/guide/).

Build settings for this example

| Setting | Value | Reason |
| --- | --- | --- |
| Build script | `vite build` through `npm run build` | Produces the browser assets instead of starting the development server. |
| Output directory | `dist` | Contains the HTML entry point and bundled assets. Substitute your configured `build.outDir` if different. |
| Public base path | `/` for a site at its own hostname | A GitHub Pages repository subpath from an old setup may be wrong on a standalone hostname. |
| SPA routing | Enabled when creating the upload project with `--spa` | Allows a browser URL such as /guide/deep-link to reach the client entry point. |

## Start with a reproducible example or your existing app

[Download the React/Vite example](https://shipvela.com/guide-assets/react-vite-fixture.zip), extract it and open `shipvela-react-vite-fixture` in your terminal. It includes source, a lockfile, a README and a build inspector. It contains no credentials, remote API calls or automatic deployment script. The example displays the current path; it is a small routing check, not a full router library.

Commands run for the isolated example

```sh
cd shipvela-react-vite-fixture
npm ci --ignore-scripts
```

For an existing app, work in its root instead. Use its documented installation process and a committed npm lockfile. The example deliberately needs no install lifecycle scripts; do not blindly add `--ignore-scripts` to an app whose native dependencies require them. If package.json and its lockfile disagree, fix that mismatch locally instead of deploying a different dependency tree. See [npm ci](https://docs.npmjs.com/cli/v11/commands/npm-ci/).

For the example, create `.env.production.local` with these harmless demonstration values. Its `.gitignore` excludes local env files. These values are not private credentials.

Example-only build inputs

```dotenv
VITE_SITE_LABEL=Shipvela production fixture
VITE_SHOW_BANNER=false
PRIVATE_SENTINEL=PRIVATE_SENTINEL_MUST_NOT_BE_BUNDLED_72
```

The label intentionally appears in the public bundle. The unprefixed sentinel tests the default Vite exposure rule; it is not a substitute for reviewing all generated assets for sensitive content.

## Build and inspect the production files

Executed against the downloadable example

```sh
npm run build
node scripts/inspect-build.mjs
```

The local build completed and generated `dist/index.html` plus a hashed JavaScript asset. The example’s inspector returned the following values. Hashed filenames and timings can change; this check is about the artifact and its contents.

Observed local output — no cloud deployment

```json
{
  "indexHtml": true,
  "javascriptFiles": 1,
  "publicLabelPresent": true,
  "unprefixedSentinelPresent": false,
  "falseFlagEnablesBanner": false,
  "productionModePresent": true
}
```

The inspector belongs to the downloadable example, not the Shipvela CLI. For your own app, inspect its output or adapt the script. Every browser-delivered file should be treated as public, even if the originating environment variable was hidden in a dashboard.

What to publish and what to keep out

| Item | Publish? | How to handle it |
| --- | --- | --- |
| `dist/index.html` and generated JS/CSS/assets | Yes | Publish the output directory as a complete build. Do not upload only index.html. |
| Source project root with package.json | No | Shipvela rejects that root; select dist or your actual build output. |
| node_modules, local env files, credentials, local databases and source maps | No | Known types are excluded by the CLI, but review your output rather than relying on filename filtering alone. |
| A secret embedded in a JS or HTML string | Never | Removing an env file does not remove an already-bundled secret. Rotate an exposed credential and rebuild. |
| Symlinks inside the output | No | Replace with intended real public files; the CLI rejects symlinks. |

The upload limit is 50 MB and 5,000 files. Large media may need an appropriate separately managed asset service or a deliberate reduction in output; do not upload the source tree to get around an output problem.

## Check a local production preview

Preview the built files locally

```sh
npm run preview -- --host 127.0.0.1
```

Open the URL printed by Vite, then visit `/guide/deep-link` directly. In the example the heading should show the configured label and the path line should reflect that URL. This browser inspection is a step for you to perform; the recorded fixture evidence covers local compilation and HTTP responses, not a live hosted browser session.

Check the Network panel for failed JavaScript, CSS and image requests. A page that works under `npm run dev` can still fail when its production asset URLs or configuration differ. Stop preview with Ctrl+C after inspection. [Vite preview is a local inspection tool](https://vite.dev/guide/static-deploy.html), not the server you keep running in production.

## Publish the reviewed output with the existing CLI

Live procedure — run only for the project you intend to publish

```sh
shipvela login
shipvela publish dist --name my-react-site --spa --json
```

Login opens Shipvela’s approval page. Compare the terminal code and approve only the request you initiated. The first publish creates an upload project and writes `shipvela.json`; the CLI sends the archive to Shipvela’s authenticated validation endpoint and waits for the particular hosting job it starts.

If a project link already exists, the publish updates that project. `--name` and `--spa` are creation choices; adding them to a later publish does not create a separate site or repair an existing routing configuration. Check the linked project’s build settings if it was created with the wrong routing mode.

With `--json`, progress is on stderr and the final result or error object is on stdout. A successful result contains `projectId`, `job` and `url`; verify that the job status is `SUCCEED`. A login response, packaging message or upload acknowledgement is not the final deployment receipt. No hosted receipt is fabricated in this article.

Read-only follow-up; replace JOB_ID with the actual numeric job ID

```sh
shipvela status --json
shipvela logs --job JOB_ID --json
```

The CLI waits by default for publish. If it times out, the job may still be running. Inspect status before repeating the mutation. The CLI does not automatically retry a publish mutation merely because a response was lost.

## Verify the hosted page, assets and direct routes

- Open the actual returned HTTPS URL and confirm a visible detail from the intended build. Do not copy a URL from an earlier unrelated project.
- Load a direct client route in a new tab and refresh it. This checks the hosting fallback as well as the app’s route handling.
- Inspect the JavaScript/CSS requests that the HTML references. They should load with the expected content, not a 404 page or HTML fallback disguised as a script.
- Exercise a meaningful interaction. If the app calls an external API, check its production URL, authorization and CORS behavior separately; successful hosting does not configure that API.
- Retain the job ID and build version in your release notes. Add a [custom domain](https://shipvela.com/guides/custom-domain-dns-https) only after the default hosted URL works.

For an update, make the intended source change, run the build again, inspect the output, then run `shipvela publish dist --json` from the same linked project root. An old dist folder republishes old code even when your source files changed.

## Diagnose the failing layer before publishing again

Concrete symptoms and recovery actions

| Symptom | Check first | Recovery |
| --- | --- | --- |
| “The directory needs an index.html” | Did the build succeed, and is this its actual output directory? | Build first; publish the directory containing the generated entry point. |
| Build exits with dependency or TypeScript errors | The first actionable build error and the local Node/lockfile versions. | Fix the source or dependency mismatch and rerun the build. Uploading cannot repair a failed compilation. |
| Blank page with asset 404s | The exact script/image URL in Network and the generated HTML. | Correct the app’s base path or asset reference, rebuild and publish the entire output. |
| A script request returns HTML | Response content type/body and whether a missing script path was rewritten. | Fix the missing asset or path; do not treat a 200 status alone as proof that JavaScript loaded. |
| Root works but a direct client URL fails | Hosting SPA mode and whether the app implements that route. | For a new SPA upload use --spa; for an existing project inspect routing settings. Verify again after deployment. |
| API requests fail but the UI renders | Production API URL, browser error, API response and allowed origin. | Correct the external service configuration or public build value; rebuild if that value was compiled into the frontend. |
| Old open tabs fail to load a dynamic import after an update | Whether the tab requests an old hashed chunk absent from the current deployment. | Test a fresh page load; investigate HTML/service-worker caching and the app’s update behavior. Do not promise a hosting rollback that is not available. |
| CLI wait times out or connection drops | The exact started job in status/logs. | Wait or diagnose that job before retrying; a lost response does not imply cancellation. |

For base-path and stale-chunk behavior, consult [Vite’s production build guide](https://vite.dev/guide/build.html). For imported versus public assets, use [Vite’s asset guide](https://vite.dev/guide/assets.html). Avoid repeatedly reloading on every error: a broken path will remain broken and a reload can discard unsaved user input.

## What was checked, and what still needs your acceptance test

The downloadable example was compiled locally with the versions listed above. Its generated bundle passed the shown inspection. Separate isolated tests exercised the real CLI archive packager, exclusion and rejection rules, and CLI control flow against clearly synthetic responses. These checks do not certify your app or prove that a production deployment, custom domain, OAuth flow or Claude Code session succeeded.

This workflow serves a static frontend. It does not provision databases, run arbitrary backend processes, or guarantee compatibility with every AI-generated project. When your own successful job, HTTPS URL, direct routes and key interaction pass, you have completed the deployment acceptance check for that build.

## Next step

[Create an account to publish your static site](https://shipvela.com/signup)

[View this guide on Shipvela](https://shipvela.com/guides/deploy-react-vite-production)
