Skip to main content

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

    Why 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 typeCache-ControlWhy
    /assets/*.{js,css,webp}max-age=31536000, immutableHashed → safe for 1 year. immutable tells browsers "don't even revalidate."
    /index.htmlno-cache, must-revalidateAlways re-checked so a publish goes live within a refresh.
    /sitemap.xml, /robots.txtmax-age=3600Short cache — refresh hourly for crawlers.
    Edge function responsesSet in code per routeDefault 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?

    1

    Browser asks the CDN for /portfolio/wayfinderroutes

    No file exists at that path on disk.

    2

    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.

    3

    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 _redirects file (Netlify convention — Lovable hosting ignores it)
    • netlify.toml / vercel.json
    • HashRouter with # 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.