Skip to content

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.

SettingExample
Build commandnpm ci && npm run build
Publish directorydist (Vite), .vitepress/dist, build (CRA), out (Next export)
Auto-deployOn

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

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, immutable

3. 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

yaml
routes:
  - type: rewrite
    source: /*
    destination: /index.html

Serve 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

PlatformFix
Renderroutes: rewrite in render.yaml
Netlify/* /index.html 200 in _redirects
VercelAutomatic for detected frameworks
Nginxtry_files $uri $uri/ /index.html;
S3 + CloudFrontError 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.

yaml
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-cache

Why 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

yaml
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
HeaderPrevents
X-Frame-Options: DENYClickjacking — your site framed by an attacker
X-Content-Type-Options: nosniffThe browser guessing a content type and executing an upload as script
Referrer-PolicyLeaking full URLs (with tokens in query strings) to third parties
Strict-Transport-SecurityDowngrade to HTTP
Content-Security-PolicyXSS — 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

yaml
routes:
  - type: redirect
    source: /old-blog/*
    destination: /blog/:splat

  - type: rewrite
    source: /api/*
    destination: https://api.example.com/:splat

Redirect 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

yaml
envVars:
  - key: VITE_API_URL
    value: https://api.example.com

Build-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

bash
npm run build
grep -r "sk_live\|secret" dist/    # should return nothing

Worth adding to CI for any project that touches payment or auth credentials.


8. Preview Deployments

yaml
pullRequestPreviewsEnabled: true

Every 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 SiteWeb Service
Server processNoneYes
CostVery lowPer instance
Free tier spin-downNoYes
SSRNoYes
API routesNoYes
Environment variablesBuild-time, publicRuntime, can be secret
ScalingAutomatic via CDNInstances

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

SymptomCause
404 on refresh at any routeMissing SPA rewrite to /index.html
New deploy not visible to returning usersindex.html is being cached
CSS/JS 404 after deployWrong publish directory, or a base path mismatch
Assets load from the wrong pathbase / basePath not set for a subdirectory deploy
Build succeeds, blank pageCheck the browser console — usually a base path or a runtime error
Env var undefined in the browserMissing the framework prefix, or set after the build
Old content after a redeployCDN cache — purge it, and fix the Cache-Control headers

The Blank Page Debug Order

  1. Open DevTools → Console — is there a JavaScript error?
  2. Network tab — are the JS/CSS files 200 or 404?
  3. If 404, the publish directory or the base path is wrong
  4. View source — does index.html reference 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.

© 2025 DDocs · Dipak's Documentation Guide