Always be curious. Have fun. Be extraordinary.
Deployment & Hosting
How a publish becomes bytes on a CDN — content-hashed assets, layered cache headers, and SPA routing that just works on refresh.
Pipeline
What happens on publish
Frontend changes
Built once, uploaded, atomically swapped.
Vite runs rollup in production mode → tree-shakes unused exports, code-splits per route, minifies, and emits content-hashed filenames into dist/. The whole folder uploads to the CDN, then traffic flips to the new build. Requires clicking Update in the publish dialog to go live.
Backend changes
Deploy on save. Live immediately.
Edge functions and database migrations push instantly when written — no publish step. Each function is a Deno bundle running in V8 isolates at the edge. Cold starts are ~50ms; warm calls are sub-10ms.
Cache busting
Content-hashed filenames
Every JS, CSS, and processed asset gets a hash derived from its bytes. Change one character of source → the hash changes → it's a brand-new URL the browser has never seen, so caches can't serve stale code.
Sample build output
dist/
├─ index.html ← never cached aggressively
├─ assets/
│ ├─ index-D8a3F1b2.js ← entry bundle (hashed)
│ ├─ Performance-aB7c2dE9.js ← route-split chunk
│ ├─ recharts-9fK2mNpQ.js ← vendor chunk
│ ├─ index-7HxJk2vL.css ← styles (hashed)
│ ├─ aaron-playful-laugh-Q3xT8w.webp ← optimized image
│ └─ aaron-filmstrip-Rk2pV7.webpWhy it's safe to cache forever
The filename is the version number. A new deploy emits new filenames; old filenames keep working for users mid-session. There's no purge step, no race condition between HTML and JS.
Image variants too
vite-imagetools resizes and re-encodes imports through sharp — the resulting WebP files are hashed alongside JS. That's how the hero went from 1.5MB JPEG to 32KB WebP without any manual export.
Edge headers
Two cache strategies, one rule
The CDN serves two kinds of files. Hashed assets get the maximum cache; unhashed entry points get a short cache so visitors see new builds within seconds of publish.
| File type | Cache-Control | Why |
|---|---|---|
| /assets/*.{js,css,webp} | max-age=31536000, immutable | Hashed → safe for 1 year. immutable tells browsers "don't even revalidate." |
| /index.html | no-cache, must-revalidate | Always re-checked so a publish goes live within a refresh. |
| /sitemap.xml, /robots.txt | max-age=3600 | Short cache — refresh hourly for crawlers. |
| Edge function responses | Set in code per route | Default no-cache; opt into edge caching only when safe. |
The "stale HTML pointing at deleted JS" trap
Long-cached HTML + freshly hashed JS = 404s on the next deploy. The fix is the inversion above: HTML is short-cached, assets are infinite-cached. New deploys flip the HTML pointer; old asset URLs stay valid for users mid-session.
Routing
How SPA routing survives a refresh
The site is a single-page app. The server only ships index.html; React Router handles every URL after boot. So how does refreshing on /portfolio/wayfinderroutes not 404?
Browser asks the CDN for /portfolio/wayfinderroutes
No file exists at that path on disk.
CDN's SPA fallback kicks in
The hosting layer sees no file, no asset extension, and that the request looks like a browser navigation. It rewrites the response to serve /index.html with a 200 status.
React Router takes the URL from there
On boot, BrowserRouter reads window.location.pathname and renders the matching route. No client-side navigation needed.
What you don't need
- ✗ A
_redirectsfile (Netlify convention — Lovable hosting ignores it) - ✗
netlify.toml/vercel.json - ✗
HashRouterwith#URLs as a workaround - ✗ A custom Express/Node server to handle the fallback
The fallback is built into the hosting layer. Files with extensions (e.g. /data.json) skip the fallback and 404 normally if missing — which is correct asset behavior.
First paint
The request waterfall
0ms GET / → CDN edge (HTML, ~3KB gzipped) 20ms GET /assets/index-D8a3F1b2.js ← parsed from <script> 20ms GET /assets/index-7HxJk2vL.css ← parsed from <link> 20ms GET /assets/aaron-playful-laugh-Q3xT8w.webp ← <link rel=preload> 80ms React boots, hydrates the shell 120ms Route component mounts → triggers code-split fetch 180ms GET /assets/Portfolio-aB7c2dE9.js ← only if needed 220ms First contentful paint ~900ms LCP (hero image painted)
Every line item after the first is served from a hashed URL with a 1-year cache. Repeat visits skip steps 2–6 entirely.
Recovery
Rollback is one click
Because each publish is an immutable build folder, rolling back is just pointing the live alias at a previous build's folder. No re-bundling, no cache purge required — the previous build's HTML still references its own (still-cached) hashed assets.
