Make a framework emit a static bundle that Capacitor can copy into the native projects, and wire the scripts so build -> cap sync is one step.
When to Use
TRIGGER when:
- Adding Capacitor to a Next.js, Nuxt, Angular, SvelteKit, Vite (React/Vue/Svelte/Solid), React Router, Qwik, or Astro project.
npx cap syncfails withThe web assets directory (./dist) must contain an index.html file.orCould not find the web assets directory: ./dist.- The app launches to a white screen or loads the wrong page after a reload or deep link.
- The framework build fails because of SSR-only features (
output: 'export'errors,prerendererrors, API routes, middleware, server actions).
Do not use for:
- Store readiness, native UX, permissions, billing, review risk ->
webapp-to-capacitor,capacitor-app-store,capacitor-apple-review-preflight. - Cordova/PhoneGap apps ->
cordova-to-capacitor. - Capacitor version upgrades ->
capacitor-app-upgrades. - Live updates setup ->
capgo-live-updates.
Live Project Snapshot
Detected framework and build dependencies:
!node -e "const fs=require('fs');if(!fs.existsSync('package.json'))process.exit(0);const pkg=JSON.parse(fs.readFileSync('package.json','utf8'));const matchers=['next','nuxt','react','react-router','@react-router/dev','vue','@angular/core','@angular/build','@sveltejs/kit','@sveltejs/adapter-static','@builder.io/qwik','@qwik.dev/core','astro','@remix-run/react','solid-js','@solidjs/start','vite','@capacitor/core','@capacitor/cli'];const out=[];for(const section of ['dependencies','devDependencies']){for(const [name,version] of Object.entries(pkg[section]||{})){if(matchers.includes(name))out.push(section+'.'+name+'='+version)}}for(const [name,cmd] of Object.entries(pkg.scripts||{})){if(['build','export','generate','sync','cap:sync'].includes(name))out.push('scripts.'+name+'='+cmd)}console.log(out.join('\n'))"
Relevant framework and Capacitor config paths:
!find . -maxdepth 3 -not -path '*/node_modules/*' \( -name 'next.config.*' -o -name 'nuxt.config.*' -o -name 'vite.config.*' -o -name 'angular.json' -o -name 'svelte.config.*' -o -name 'react-router.config.*' -o -name 'astro.config.*' -o -name 'capacitor.config.json' -o -name 'capacitor.config.ts' -o -name 'capacitor.config.js' \)
Core Facts (apply to every framework)
- Capacitor copies
webDirinto the native app. It never runs Node. SSR, API routes, middleware, ISR, server actions,cookies()/headers(), and image optimization servers do not exist at runtime. Move them to a hosted backend and call it over HTTPS. - The app is served from
capacitor://localhost(iOS) andhttps://localhost(Android). Your backend must allow those origins for CORS, or useCapacitorHttp. - Capacitor's local server returns the root
index.htmlfor any path without a file extension. History-mode SPA routing (createWebHistory,BrowserRouter, Angular defaultPathLocationStrategy) works; hash routing is not required. - The same fallback means per-route HTML files (Next.js export, SvelteKit prerender, Astro) are never served for extensionless URLs. A cold start or
location.reload()on/settings/loads the rootindex.html. Keep navigation client-side and route deep links throughApp.addListener('appUrlOpen', ...)+ your router. - Asset paths: keep the framework's base at
/(Vitebase, Angular<base href="/">, NextbasePath/assetPrefixunset). A CDN or sub-path base used for web hosting breaks asset loading in the app; use a separate mobile build config. - Environment variables are inlined at build time (
NEXT_PUBLIC_*,VITE_*,NUXT_PUBLIC_*,PUBLIC_*in SvelteKit, Angularenvironment.ts). Rebuild before everycap sync; never point production builds atlocalhost. - Capacitor 8 needs Node 22+. Capacitor 9 (currently
@next, not GA) needs Node 24+. Some framework majors also raise Node minimums (SvelteKit 3: Node 22.17+).
Procedure
- Read the snapshot. Identify framework, major version, and the current build output directory. If several apps live in a monorepo, ask which one ships to mobile.
- Load the matching reference (only the one in play):
| Framework in package.json | Load | webDir |
|---|---|---|
next |
references/nextjs.md |
out |
nuxt |
references/vue-nuxt.md |
.output/public |
vue + vite (no Nuxt) |
references/vue-nuxt.md |
dist |
react + vite, react-router/@react-router/dev, CRA |
references/react.md |
dist, build/client, or build |
@angular/core |
references/angular.md |
dist/<project>/browser |
@sveltejs/kit or svelte + vite |
references/svelte.md |
build or dist |
solid-js, @builder.io/qwik/@qwik.dev/core, astro, @remix-run/* |
references/other-frameworks.md |
varies |
- Inventory server-only features before changing config (grep for
app/api/,pages/api/,middleware.ts/proxy.ts,'use server',getServerSideProps,+page.server.ts,+server.ts,server/api/,@angular/ssr). Report the list to the user and agree where each moves (external API, client fetch, or native plugin) before deleting anything. - Make the static build work and inspect the output directory: it must contain
index.htmlat its root. - Install and initialise Capacitor (use the repo's package manager for installs):
npm install @capacitor/core @capacitor/ios @capacitor/android
npm install -D @capacitor/cli
npx cap init "App Name" com.company.app --web-dir <webDir>
npx cap add ios
npx cap add android
- Add one script that always builds before syncing, e.g.
"build:mobile": "<framework build> && cap sync". - Run the verification below on both platforms.
Minimal config (androidScheme: 'https' is the default since Capacitor 6; only set it to keep an older origin):
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.company.app',
appName: 'App Name',
webDir: 'dist',
};
export default config;
Live Reload
Capacitor 8: npx cap run ios -l --host <LAN-IP> --port <port>. Capacitor 9: npx cap run ios --url http://<LAN-IP>:<port> (the -l/--host/--port/--https flags were merged into --url). The dev server must listen on the LAN (--host 0.0.0.0 / server.host: true in Vite). Never commit server.url into capacitor.config.*.
Verification
npm run build
test -f <webDir>/index.html && echo "index.html OK"
npx cap sync
npx cap run ios # or open in Xcode
npx cap run android
Then on device/simulator:
- App opens on the first screen without a white flash beyond the splash.
- Navigate to a nested route, background + resume, and confirm the route survives.
- Open a deep link (
xcrun simctl openurl booted <url>/adb shell am start -a android.intent.action.VIEW -d <url>) and confirm it lands on the right screen. - Inspect the WebView (Safari Web Inspector /
chrome://inspect) for 404s on JS/CSS and CORS errors. grep -rn "localhost:\|127.0.0.1" <webDir>returns nothing unexpected.
Error Handling
| Symptom / error | Cause | Fix |
|---|---|---|
The web assets directory (./dist) must contain an index.html file. |
webDir points at the wrong folder or SSR build has no index.html |
Set webDir to the static output listed above; enable static/SPA mode |
Could not find the web assets directory: ./out. |
Build not run or different output dir | Run the build first; match webDir to the real folder |
White screen, console Failed to load resource for /_next/... or /assets/... |
Non-root base path / CDN assetPrefix |
Use / base for the mobile build |
| White screen, no 404s | JS error at boot (often window/document during prerender, or SSR-only import) |
Inspect WebView console; guard browser-only code |
| Reload or deep link opens home page or hydration error | Capacitor serves root index.html for extensionless paths |
Client-side navigation + appUrlOpen handler; see framework reference |
Page "/x/[id]" is missing "generateStaticParams()" (Next.js) |
Dynamic route in export mode | Pre-generate params or switch to a query-string/client route |
Image Optimization using the default loader is not compatible with output: export |
next/image default loader |
images.unoptimized: true or custom loader |
| API call works in browser, fails on device | CORS for capacitor://localhost / https://localhost, or localhost URL baked in |
Allow origins on backend, or enable CapacitorHttp; fix env var |
References
references/nextjs.md- Next.js 14-16 static export, App Router traps, dynamic routes, images.references/react.md- Vite React, React Router framework SPA mode, CRA legacy.references/vue-nuxt.md- Vue + Vite, Nuxt 4 SPA generate.references/angular.md- Angular 17+ application builder output, SSR removal.references/svelte.md- SvelteKit 2 and 3 with adapter-static, plain Svelte + Vite.references/other-frameworks.md- Solid, Qwik, Astro, Remix v2.
Related skills: webapp-to-capacitor (store readiness), capacitor-deep-linking, safe-area-handling, capgo-live-updates.