SPA → SSR (Next.js / Nuxt / SvelteKit)
Weeks Med riskMove a Vite SPA (React, Vue, or Svelte) onto a server-rendered framework (Next.js, Nuxt, or SvelteKit respectively) for SEO, faster TTFB, and real route-level data loading.
The Mushi-specific bits are: env-var prefix changes, where to mount the provider so it survives navigation, and what to do about hydration warnings. The framework-level migration mechanics are upstream:
- React SPA → Next.js App Router: official Next.js migration guide
- Vue SPA → Nuxt 3: official Nuxt migration guide
- Svelte SPA → SvelteKit: official SvelteKit migration guide
Env-var prefix mapping
This is the change that bites every team:
| Vite SPA | Next.js | Nuxt | SvelteKit |
|---|---|---|---|
VITE_MUSHI_PROJECT_ID | NEXT_PUBLIC_MUSHI_PROJECT_ID | NUXT_PUBLIC_MUSHI_PROJECT_ID | PUBLIC_MUSHI_PROJECT_ID |
VITE_MUSHI_API_KEY | NEXT_PUBLIC_MUSHI_API_KEY | NUXT_PUBLIC_MUSHI_API_KEY | PUBLIC_MUSHI_API_KEY |
import.meta.env.VITE_* | process.env.NEXT_PUBLIC_* | useRuntimeConfig().public.* | import.meta.env.PUBLIC_* |
The Mushi CLI handles this automatically: rerun npx mushi-mushi init
after the migration and it’ll write the right prefix into your .env.local.
Migration checklist (generic, picks the right framework as you go)
Migration checklist
12 required steps
- Step 01Pick the target framework
- Step 02Bootstrap the new framework alongside the old SPA
- Step 03Port shared design tokens / global CSS first
- Step 04Migrate routes one at a time
- Step 05Move data fetching to the server
- Step 06Rename env vars per the table above
- Step 07Mount Mushi at the root of the new framework
- Step 08Verify no hydration warnings
- Step 09For Nuxt/Next, use a .client suffix or "use client" for Mushi components
- Step 10Confirm SSR routes are crawlable
- Step 11Redirect old SPA routes to the new ones (when all migrated)
- Step 12Smoke-test Mushi from a server-rendered page
Where the Mushi widget lives across SSR vs CSR
The Mushi visual widget mounts client-side, in a Shadow DOM child of
document.body, after Mushi.init() runs. Because it doesn’t render in
the server-side HTML, you’ll see:
- ✅ No hydration warnings caused by the widget itself
- ✅ Crawlers don’t see the widget (it’s not in the initial HTML)
- ✅ The widget appears within ~100 ms of
Mushi.init()resolving - ⚠️ Brief flash of un-widget’d page on slow networks (acceptable; this is intentional client-only behaviour to keep server HTML lean)
Common gotchas
useMushi()in a Server Component (Next.js App Router) — build error. Add'use client'. See the Next.js Pages → App Router guide for the recommended pattern.- Forgetting to call
Mushi.init()in addition to the framework adapter — Vue and Svelte adapters do NOT mount the widget themselves. They wire composables / error handlers; you still needMushi.init(credentials)to mount the visual widget. - Env-var prefix typos — symptom is
projectId: undefinederrors at runtime. Runnpx mushi-mushi initto regenerate the env section correctly.
References
Last updated on