---
title: "SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild"
aliases: [spa-cache-control, vite-cache-broken-spa, index-html-no-cache]
tags: [vite, spa, caching, apache, deployment, gotcha, react]
sources:
  - "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---

# SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild

After a Vite rebuild, JavaScript chunk filenames change (content-hash in filename). If the browser has cached the old `index.html`, it will request the old hash filenames — which no longer exist — and the SPA fails to load with 404 errors on JS/CSS chunks. The fix is to instruct browsers and CDNs to never cache `index.html` while allowing long-lived caching for the hashed asset files.

## Key Takeaways

- Vite generates filenames like `main-BfD92kXz.js` — hash changes on every rebuild
- Old `index.html` cached in browser references old hash → 404 on JS chunks → blank page or partial load
- `index.html` must always be served with `Cache-Control: no-cache, no-store, must-revalidate`
- Hashed assets (`*.js`, `*.css` in `/assets/`) can be cached indefinitely (`max-age=31536000, immutable`)
- The symptom is a white screen or console errors like `Failed to load module script` after deploying

## Details

### Apache: Two-Rule Cache Pattern

```apache
<VirtualHost *:443>
    DocumentRoot /var/www/html/my-spa

    # index.html — never cache (Vite rewrites it on every build)
    <Files "index.html">
        Header always set Cache-Control "no-cache, no-store, must-revalidate"
        Header always set Pragma "no-cache"
        Header always set Expires "0"
    </Files>

    # Hashed assets — cache forever (hash in filename guarantees uniqueness)
    <LocationMatch "^/assets/.*\.(js|css|woff2?|png|svg|ico)$">
        Header always set Cache-Control "public, max-age=31536000, immutable"
    </LocationMatch>

    # SPA fallback — all routes serve index.html
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ /index.html [L]
</VirtualHost>
```

### Nginx Equivalent

```nginx
server {
    root /var/www/html/my-spa;

    # Never cache index.html
    location = /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }

    # Cache hashed Vite assets forever
    location /assets/ {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # SPA fallback
    location / {
        try_files $uri $uri/ /index.html;
    }
}
```

### Diagnosing the Problem

When users report "blank screen after deployment" or "old version still showing":

1. Open DevTools → Network → filter by `index.html`
2. Check Response Headers for `Cache-Control`
3. If `max-age > 0` or `Cache-Control` is absent — this is the bug
4. Hard refresh (`Ctrl+Shift+R`) will load the new version, confirming cache as root cause
5. Check console for `Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html"` — this is the 404-as-HTML fallback

### Why Hashed Assets Can Have Infinite Cache

Vite's build output:
```
dist/
  index.html              ← changes every build (references new hashes)
  assets/
    main-BfD92kXz.js      ← hash in filename; content is immutable for this URL
    vendor-CqRt4mPn.js
    index-Dx8kLpWw.css
```

Because the filename itself is the cache key and contains the content hash, old URLs are simply abandoned and new hashes take their place. There is no risk of stale content for hashed files.

> [!note] Same issue affects sub-path deployments
> When serving a SPA at a prefix (e.g. `/dashboard/`), the `<Files "index.html">` directive must cover the actual served path. If using [[wiki/concepts/vite-prebuilt-subpath-workaround]] with `<base href>`, verify the same no-cache rule covers the entry point.

## Related

- [[wiki/concepts/vite-prebuilt-subpath-workaround]] — Vite `<base href>` pattern for sub-path deployments
- [[wiki/concepts/shell-static-deploy-patterns]] — static deploy scripts: cp, rsync, Apache reload
- [[wiki/tech-patterns/react-vite-typescript]] — standard Oliver SPA stack

## Sources

- [[daily/2026-05-06.md]] — BAIC dashboard: blank screen after Vite rebuild; old index.html referencing non-existent chunk hashes
