JavaScript apps are still files at the origin

A JavaScript application—React, Vue, Svelte, Lit, Alpine-enhanced pages, vanilla ES modules—ships as .js, .css, and .html (plus assets). StaticHost hosts those files. The app’s brain runs in the visitor’s browser and calls APIs on other origins. That split is the whole idea.

What StaticHost does not provide: Node to execute your app server-side, remote npm install, platform functions, or nginx soft-fallback for client routers. try_files $uri $uri/ =404 remains the law. Preview HTTPS is immediate; custom domain certificates arrive after DNS verifies. Plans are Starter $9, Pro $30, Scale $65, Business $130 after a ~1 day trial—no forever-free, no email, no cPanel, no built-in CDN product.

If that paragraph feels constraining, good: it is better to feel constrained before you migrate than after cutover.

Pick a shipping shape

ShapeServer pathsFit on StaticHost
Multipage HTML + sprinkles of JSReal HTML filesExcellent
SPA + hash router/ + #/…Good
SPA + history router + prerendered filesReal files per routeGood
SPA + history router, no files for routesMissing paths 404Bad here
SSR app (next start, etc.)Needs NodeWrong host

Framework deep dives: React, Vue, Vite, Svelte static, Next static export, SPA overview.

Build and publish loop

npm ci
npm run build
cd dist  # or build/ or out/
zip -r ../js-app.zip .
unzip -l ../js-app.zip | head

Upload or use a GitHub artifact branch—git deploy a static site, host a dist folder online. Verify on HTTPS preview. Roll back from history. Attach a custom domain only after the preview behaves.

Environment variables belong only in public prefixes (VITE_, REACT_APP_, PUBLIC_, NEXT_PUBLIC_) and bake in at build time. Grep the bundle for accidental secrets before publish:

grep -R "SECRET\|API_KEY\|private_key" dist || true

If something sensitive appears, rotate credentials and rebuild with a corrected env strategy. StaticHost cannot un-bake a secret from a previous deploy that browsers already cached—roll back and invalidate carefully on your side (service workers especially).

Worked example: Vite vanilla TS app with hash routes

npm create vite@latest widget-app -- --template vanilla-ts
cd widget-app
npm ci

In the router or navigation code, prefer hash-based locations if you need multi-screen UX without prerender:

// minimal illustration — navigate via hash
location.hash = '#/settings'
npm run build
cd dist && zip -r ../../widget.zip .

Deploy widget.zip. Open preview. Hard-refresh on /#/settings—works. Hard-refresh on /settings without a file—404 by design. That contrast is the product teaching you how nginx is configured.

Browser platform concerns (still yours)

  • CORS on APIs for both preview and production hosts
  • Auth via external IdPs; no server sessions on this origin
  • Storage (localStorage / IndexedDB) caveats across subdomains
  • Service workers caching old deploys after a rollback
  • Module paths all present on the server (no transform-on-request)

Publishing JS without a bundler is valid for small apps—how to publish HTML, CSS, and JavaScript files—as long as every imported file exists at its URL.

Performance without a CDN product

Code-split large apps; do not celebrate a 3 MB single bundle on a host without a built-in CDN product. Craft notes: fast static site hosting, lightweight high-performance static hosting. Measure on a throttled mobile profile. Cache-bust hashed filenames from your bundler; avoid infinite-cache service workers until you have an update strategy.

Plans

Starter $9 (1×2 GB), Pro $30 (3×10 GB + staging) for staging builds with staging API URLs, Scale $65 (10×30 GB), Business $130 (30×100 GB + teams). Trial ~1 day. No PHP/WordPress runtime here.

When not to put a JS app on StaticHost

  • You need SSR for SEO on personalized pages every request.
  • You need server-side sessions sticky to this origin.
  • Your router strategy is history mode and you refuse prerender or hash routes.
  • You expected remote npm builds as part of “git push.”

Failure table (JS apps)

SymptomLikely causeFix
Blank page, 404 on /assets/index-….jsNested zip or wrong baseRe-zip dist contents; set Vite base: '/'
API works locally, fails on previewCORS allowlist missing preview hostAdd preview and production origins
Users see old bundle after fixService worker or long cacheBump SW version; hashed assets help
Deep link 404History router without filesHashRouter, prerender, or different host
Secret in client bundleEnv without public prefix disciplineRotate; rebuild; stop inlining secrets

Extra procedure: map an ES module graph without a bundler

Small apps can ship raw modules if every file exists at its URL:

app/
  index.html
  main.js
  lib/math.js
  lib/dom.js

main.js:

import { sum } from './lib/math.js'
import { qs } from './lib/dom.js'
qs('#out').textContent = String(sum(2, 2))

Zip app/ contents, deploy, open HTTPS preview. If lib/math.js 404s, the graph breaks—there is no transform-on-request. Bundlers collapse this problem; hand-written graphs demand discipline. Either way, history-mode client routes still need files or hashes because nginx answers =404 for missing paths.

Second worked path: promote a fingerprinted bundle

  1. Build with the bundler so filenames include content hashes.
  2. Grep dist for secrets before publish.
  3. Deploy; verify on preview.
  4. Roll back once on purpose so the team knows the control.
  5. Only then DNS → verify → certificate.

Plans: Starter $9, Pro $30 (staging builds with staging API URLs), Scale $65, Business $130. No forever-free after ~1 day trial; no email; no cPanel; no built-in CDN product; no PHP/WordPress runtime. Performance notes: lightweight high-performance static hosting.

Failure-adjacent procedure: CORS and preview origins

When the UI loads but API calls fail only on StaticHost preview:

  1. Note the exact preview origin from the address bar.
  2. Add it to the API allowlist alongside the future production hostname.
  3. Rebuild only if the API base URL itself was wrong; CORS fixes live on the API.
  4. Retest login/token refresh on preview HTTPS.
  5. After DNS cutover, confirm the custom origin is also allowlisted—certificate timing does not fix CORS.

StaticHost will not proxy your API or invent server sessions. The browser talks to your API; the origin merely serves JS. Keep that split loud in design reviews so nobody “temporarily” puts secrets into the bundle.

FAQ

Is a JS app “dynamic” or “static”?

The origin is static; the UI can be dynamic in the browser.

Can I use WebSockets?

From the browser to a WebSocket server elsewhere, yes. StaticHost is not that server.

Do I need TypeScript compiled?

Yes—publish JS (or WASM), not raw project TS expecting a transform on the server.

Why do imports fail only on deep routes?

Base href / router base / asset path issues, or you expected SPA fallback. Check the Network panel.

Should small businesses use JS apps for brochures?

Often overkill—static hosting for small business. Use JS when interactivity needs it.