Static Sites on Render
For anything with no server: a Vite/React SPA, a VitePress or Astro docs site, a Next.js static export.
1. Setup
Dashboard → New → Static Site → connect the repository.
| Setting | Example |
|---|---|
| Build command | npm ci && npm run build |
| Publish directory | dist (Vite), .vitepress/dist, build (CRA), out (Next export) |
| Auto-deploy | On |
Static sites are served from Render's CDN. There is no container, no cold start, and the free tier does not spin down — unlike free web services.
2. render.yaml
services:
- type: web
name: docs
runtime: static
buildCommand: npm ci && npm run build
staticPublishPath: ./docs/.vitepress/dist
pullRequestPreviewsEnabled: true
routes:
- type: rewrite
source: /*
destination: /index.html # SPA fallback
headers:
- path: /*
name: X-Frame-Options
value: DENY
- path: /assets/*
name: Cache-Control
value: public, max-age=31536000, immutable3. The SPA Rewrite (Most Common Problem)
A React Router app deploys fine, / works, then a refresh on /about returns 404.
Why
Client-side routing means the server has no /about file. The router only takes over after index.html loads and JavaScript runs. A direct request or a refresh asks the server for a path that does not exist on disk.
Fix
routes:
- type: rewrite
source: /*
destination: /index.htmlServe index.html for every path; the router then reads the URL and renders the right view.
Rewrite, not redirect. A rewrite keeps the URL as /about so the router can read it. A redirect would change the URL to / and lose the route.
The Same Problem Everywhere
| Platform | Fix |
|---|---|
| Render | routes: rewrite in render.yaml |
| Netlify | /* /index.html 200 in _redirects |
| Vercel | Automatic for detected frameworks |
| Nginx | try_files $uri $uri/ /index.html; |
| S3 + CloudFront | Error document → index.html |
| Apache | .htaccess rewrite rules |
This is a very common interview question — "your SPA 404s on refresh, why?" — because it tests whether you understand the difference between client-side and server-side routing rather than just using a router.
4. Caching Headers
The single highest-impact static hosting configuration.
headers:
# Hashed asset filenames — cache forever
- path: /assets/*
name: Cache-Control
value: public, max-age=31536000, immutable
# index.html — never cache
- path: /index.html
name: Cache-Control
value: no-cacheWhy This Split
Modern bundlers emit hashed filenames: app-a3f9c2.js. The content is the name, so the file can never change — cache it for a year.
index.html references those hashed files. If it were cached, a returning user would load the old HTML pointing at the old bundles and never see the new deploy. So index.html must always be revalidated.
"Cache the hashed assets forever, never cache the HTML" is the rule, and articulating it is a strong answer.
no-cache vs no-store
no-cache means "store it, but revalidate before using it" — the browser sends an If-None-Match and usually gets a cheap 304. no-store means "never store it at all". For index.html, no-cache is correct — you still get the 304 saving.
5. Custom Headers
headers:
- path: /*
name: X-Frame-Options
value: DENY
- path: /*
name: X-Content-Type-Options
value: nosniff
- path: /*
name: Referrer-Policy
value: strict-origin-when-cross-origin
- path: /*
name: Strict-Transport-Security
value: max-age=63072000; includeSubDomains| Header | Prevents |
|---|---|
X-Frame-Options: DENY | Clickjacking — your site framed by an attacker |
X-Content-Type-Options: nosniff | The browser guessing a content type and executing an upload as script |
Referrer-Policy | Leaking full URLs (with tokens in query strings) to third parties |
Strict-Transport-Security | Downgrade to HTTP |
Content-Security-Policy | XSS — the strongest and the hardest to configure |
Test with securityheaders.com. A static site with no headers configured scores an F, and it takes five minutes to fix.
6. Redirects
routes:
- type: redirect
source: /old-blog/*
destination: /blog/:splat
- type: rewrite
source: /api/*
destination: https://api.example.com/:splatRedirect vs Rewrite
Redirect — the browser is told to go elsewhere. The URL changes. Use for moved content.
Rewrite — the server fetches from elsewhere and returns it. The URL stays. Use to put a backend under your own domain.
Rewrite as a CORS Escape Hatch
Rewriting /api/* to your backend means the browser only ever sees your own origin — no CORS at all, no preflight requests. This is a genuinely useful trick and a good answer to "how would you avoid CORS problems".
Note the ordering rule: the SPA catch-all /* → /index.html must come last, or it swallows every other route.
7. Environment Variables in a Static Build
envVars:
- key: VITE_API_URL
value: https://api.example.comBuild-Time Only, and Public
There is no server. Variables are substituted into the JavaScript at build time and shipped to the browser.
VITE_*(Vite),NEXT_PUBLIC_*(Next),REACT_APP_*(CRA) — all public, all baked in- Never put a secret here. It is in the bundle, readable in DevTools
- Changing a value requires a rebuild, not just a restart
Verify Nothing Leaked
npm run build
grep -r "sk_live\|secret" dist/ # should return nothingWorth adding to CI for any project that touches payment or auth credentials.
8. Preview Deployments
pullRequestPreviewsEnabled: trueEvery PR gets its own URL, posted back to the pull request. Reviewers click a link instead of pulling and building the branch.
For static sites this is cheap — no container running, just files on a CDN.
9. Static Site vs Web Service
| Static Site | Web Service | |
|---|---|---|
| Server process | None | Yes |
| Cost | Very low | Per instance |
| Free tier spin-down | No | Yes |
| SSR | No | Yes |
| API routes | No | Yes |
| Environment variables | Build-time, public | Runtime, can be secret |
| Scaling | Automatic via CDN | Instances |
Which For a Next.js App?
- Static export (
output: "export") → Static Site. You lose Server Components with runtime data, Route Handlers, Server Actions, middleware, ISR and image optimisation - Full Next.js → Web Service running
next start
For a real Next.js app, use a Web Service. Static export is for genuinely static sites.
10. Common Problems
| Symptom | Cause |
|---|---|
| 404 on refresh at any route | Missing SPA rewrite to /index.html |
| New deploy not visible to returning users | index.html is being cached |
| CSS/JS 404 after deploy | Wrong publish directory, or a base path mismatch |
| Assets load from the wrong path | base / basePath not set for a subdirectory deploy |
| Build succeeds, blank page | Check the browser console — usually a base path or a runtime error |
| Env var undefined in the browser | Missing the framework prefix, or set after the build |
| Old content after a redeploy | CDN cache — purge it, and fix the Cache-Control headers |
The Blank Page Debug Order
- Open DevTools → Console — is there a JavaScript error?
- Network tab — are the JS/CSS files 200 or 404?
- If 404, the publish directory or the base path is wrong
- View source — does
index.htmlreference the paths the server actually serves?
A blank page is almost always assets 404ing because of a path mismatch, not a code bug. Knowing to check the Network tab before reading code is the practical instinct being tested.