NYC
skills/cap-go/capgo-skills/framework-to-capacitor

framework-to-capacitor

SKILL.md

Framework to Capacitor Integration

Comprehensive guide for integrating web frameworks with Capacitor to build mobile apps.

When to Use This Skill

  • Converting a Next.js app to a mobile app
  • Integrating React, Vue, Angular, or Svelte with Capacitor
  • Configuring static exports for Capacitor
  • Setting up routing for mobile apps
  • Optimizing framework builds for native platforms

Framework Support Matrix

Framework Static Export SSR Support Recommended Approach
Next.js ✅ Yes ❌ No Static export (output: 'export')
React ✅ Yes N/A Create React App or Vite
Vue ✅ Yes ❌ No Vite or Vue CLI
Angular ✅ Yes ❌ No Angular CLI
Svelte ✅ Yes ❌ No SvelteKit with adapter-static
Remix ✅ Yes ❌ No SPA mode
Solid ✅ Yes ❌ No Vite
Qwik ✅ Yes ❌ No Static site mode

CRITICAL: Capacitor requires static HTML/CSS/JS files. SSR (Server-Side Rendering) does not work in native apps.


Next.js + Capacitor

Next.js is popular for React apps. Capacitor requires static export.

Step 1: Create or Update next.config.js

For Next.js 13+ (App Router):

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  images: {
    unoptimized: true, // Required for static export
  },
  trailingSlash: true, // Helps with routing on mobile
};

module.exports = nextConfig;

For Next.js 12 (Pages Router):

// next.config.js
module.exports = {
  output: 'export',
  images: {
    unoptimized: true,
  },
  trailingSlash: true,
};

Step 2: Build Static Files

bun run build

This creates an out/ directory with static files.

Step 3: Install Capacitor

bun add @capacitor/core @capacitor/cli
bunx cap init

Configuration:

  • App name: Your app name
  • App ID: com.company.app
  • Web directory: out (Next.js static export output)

Step 4: Configure Capacitor

Create capacitor.config.ts:

import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.company.app',
  appName: 'My App',
  webDir: 'out', // Next.js static export directory
  server: {
    androidScheme: 'https',
  },
};

export default config;

Step 5: Add Platforms

bun add @capacitor/ios @capacitor/android
bunx cap add ios
bunx cap add android

Step 6: Build and Sync

# Build Next.js
bun run build

# Sync with native projects
bunx cap sync

Step 7: Run on Device

iOS:

bunx cap open ios
# Build and run in Xcode

Android:

bunx cap open android
# Build and run in Android Studio

Next.js Routing Considerations

Use hash routing for complex apps:

// next.config.js
const nextConfig = {
  output: 'export',
  basePath: '',
  assetPrefix: '',
};

Or use Next.js's built-in routing (works with trailingSlash: true).

Next.js Image Optimization

next/image doesn't work with static export. Use alternatives:

Option 1: Use standard img tag

// Instead of next/image
<img src="/images/photo.jpg" alt="Photo" />

Option 2: Use a custom Image component

// components/CapacitorImage.tsx
import { Capacitor } from '@capacitor/core';

export const CapacitorImage = ({ src, alt, ...props }) => {
  const isNative = Capacitor.isNativePlatform();
  const imageSrc = isNative ? src : src;
  
  return <img src={imageSrc} alt={alt} {...props} />;
};

Next.js API Routes

API routes don't work in static export. Use alternatives:

  1. External API: Call a separate backend
  2. Capacitor plugins: Use native features
  3. Local storage: Use @capacitor/preferences
import { Preferences } from '@capacitor/preferences';

// Save data
await Preferences.set({
  key: 'user',
  value: JSON.stringify(userData),
});

// Load data
const { value } = await Preferences.get({ key: 'user' });
const userData = JSON.parse(value || '{}');

Next.js Middleware

Middleware doesn't work in static export. Handle logic client-side:

// In your React components
import { useEffect } from 'react';
import { useRouter } from 'next/router';

export default function ProtectedPage() {
  const router = useRouter();

  useEffect(() => {
    const checkAuth = async () => {
      const { value } = await Preferences.get({ key: 'token' });
      if (!value) {
        router.push('/login');
      }
    };
    checkAuth();
  }, []);

  return <div>Protected content</div>;
}

Complete Next.js + Capacitor Example

package.json:

{
  "name": "my-capacitor-app",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "build:mobile": "next build && cap sync",
    "ios": "cap open ios",
    "android": "cap open android"
  },
  "dependencies": {
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@capacitor/core": "^6.0.0",
    "@capacitor/ios": "^6.0.0",
    "@capacitor/android": "^6.0.0",
    "@capacitor/camera": "^6.0.0"
  },
  "devDependencies": {
    "@capacitor/cli": "^6.0.0",
    "typescript": "^5.0.0"
  }
}

React + Capacitor

React works great with Capacitor using Vite or Create React App.

Option 1: Vite (Recommended)

Create new project:

bun create vite my-app --template react-ts
cd my-app
bun install

Install Capacitor:

bun add @capacitor/core @capacitor/cli
bunx cap init

Configure vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist', // Capacitor webDir
  },
});

capacitor.config.ts:

import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.company.app',
  appName: 'My App',
  webDir: 'dist',
};

export default config;

Add platforms and build:

bun add @capacitor/ios @capacitor/android
bunx cap add ios
bunx cap add android
bun run build
bunx cap sync

Option 2: Create React App

Create new project:

bunx create-react-app my-app --template typescript
cd my-app

Install Capacitor:

bun add @capacitor/core @capacitor/cli
bunx cap init

capacitor.config.ts:

const config: CapacitorConfig = {
  appId: 'com.company.app',
  appName: 'My App',
  webDir: 'build', // CRA outputs to build/
};

Build and sync:

bun run build
bunx cap sync

React Router Configuration

Use HashRouter for mobile:

import { HashRouter as Router, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  );
}

Vue + Capacitor

Vue works seamlessly with Capacitor.

Create Vue + Capacitor Project

Using Vite:

bun create vite my-app --template vue-ts
cd my-app
bun install

Install Capacitor:

bun add @capacitor/core @capacitor/cli
bunx cap init

vite.config.ts:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    outDir: 'dist',
  },
});

capacitor.config.ts:

const config: CapacitorConfig = {
  appId: 'com.company.app',
  appName: 'My App',
  webDir: 'dist',
};

Add platforms:

bun add @capacitor/ios @capacitor/android
bunx cap add ios
bunx cap add android
bun run build
bunx cap sync

Vue Router Configuration

Use hash mode for mobile:

// router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
  ],
});

export default router;

Angular + Capacitor

Angular has excellent Capacitor integration.

Create Angular + Capacitor Project

Create Angular app:

bunx @angular/cli new my-app
cd my-app

Install Capacitor:

bun add @capacitor/core @capacitor/cli
bunx cap init

capacitor.config.ts:

const config: CapacitorConfig = {
  appId: 'com.company.app',
  appName: 'My App',
  webDir: 'dist/my-app/browser', // Angular 17+ output
};

For Angular 16 and below:

webDir: 'dist/my-app',

Add platforms:

bun add @capacitor/ios @capacitor/android
bunx cap add ios
bunx cap add android
bun run build
bunx cap sync

Angular Router Configuration

HashLocationStrategy for mobile:

// app.config.ts (Angular 17+)
import { provideRouter, withHashLocation } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withHashLocation()),
  ],
};

For Angular 16 and below:

// app.module.ts
import { LocationStrategy, HashLocationStrategy } from '@angular/common';

@NgModule({
  providers: [
    { provide: LocationStrategy, useClass: HashLocationStrategy }
  ],
})
export class AppModule {}

Svelte + Capacitor

Svelte and SvelteKit work great with Capacitor.

SvelteKit + Capacitor

Create SvelteKit app:

bunx create-svelte my-app
cd my-app
bun install

Install adapter-static:

bun add -D @sveltejs/adapter-static

Configure svelte.config.js:

import adapter from '@sveltejs/adapter-static';

const config = {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: 'index.html',
    }),
  },
};

export default config;

Install Capacitor:

bun add @capacitor/core @capacitor/cli
bunx cap init

capacitor.config.ts:

const config: CapacitorConfig = {
  appId: 'com.company.app',
  appName: 'My App',
  webDir: 'build',
};

Build and sync:

bun run build
bunx cap sync

Vite + Svelte (Simpler Option)

Create with Vite:

bun create vite my-app --template svelte-ts
cd my-app
bun install

Install Capacitor:

bun add @capacitor/core @capacitor/cli
bunx cap init

capacitor.config.ts:

const config: CapacitorConfig = {
  appId: 'com.company.app',
  appName: 'My App',
  webDir: 'dist',
};

Common Patterns Across Frameworks

1. Environment Detection

Detect if running in native app:

import { Capacitor } from '@capacitor/core';

const isNative = Capacitor.isNativePlatform();
const platform = Capacitor.getPlatform(); // 'ios', 'android', or 'web'

if (isNative) {
  // Use native plugins
} else {
  // Use web APIs
}

2. Deep Linking

Handle deep links in your app:

import { App } from '@capacitor/app';

App.addListener('appUrlOpen', (data) => {
  // Handle deep link
  const slug = data.url.split('.app').pop();
  // Navigate to route
});

3. Live Updates with Capgo

Add live updates to any framework:

bun add @capgo/capacitor-updater
import { CapacitorUpdater } from '@capgo/capacitor-updater';

// Check for updates
const { id } = await CapacitorUpdater.download({
  url: 'https://api.capgo.app/updates',
});

// Apply update
await CapacitorUpdater.set({ id });

4. Native UI Components

Use Ionic Framework for any framework:

bun add @ionic/core

React:

bun add @ionic/react @ionic/react-router

Vue:

bun add @ionic/vue @ionic/vue-router

Angular:

bun add @ionic/angular

5. Storage

Use Capacitor Preferences for all frameworks:

import { Preferences } from '@capacitor/preferences';

// Set value
await Preferences.set({ key: 'theme', value: 'dark' });

// Get value
const { value } = await Preferences.get({ key: 'theme' });

// Remove value
await Preferences.remove({ key: 'theme' });

// Clear all
await Preferences.clear();

6. Camera Access

Same API across all frameworks:

import { Camera, CameraResultType } from '@capacitor/camera';

const photo = await Camera.getPhoto({
  quality: 90,
  allowEditing: true,
  resultType: CameraResultType.Uri,
});

const imageUrl = photo.webPath;

Build Scripts for All Frameworks

Add these to package.json:

{
  "scripts": {
    "dev": "vite", // or next dev, ng serve, etc.
    "build": "vite build", // or next build, ng build, etc.
    "build:mobile": "vite build && cap sync",
    "ios": "cap run ios",
    "android": "cap run android",
    "sync": "cap sync"
  }
}

Routing Best Practices

Hash vs. History Mode

Hash mode (recommended for mobile):

  • Works without server configuration
  • URLs look like: #/about
  • No server-side routing needed

History mode (requires server):

  • Clean URLs: /about
  • Requires server fallback to index.html
  • Can have issues on mobile

Recommendation: Use hash mode for Capacitor apps.


Common Issues and Solutions

Issue: Blank Screen on Mobile

Cause: Incorrect webDir or build output.

Solution:

  1. Check build output directory matches webDir in capacitor.config.ts
  2. Rebuild: bun run build
  3. Sync: bunx cap sync
  4. Check browser console in device

Issue: Routing Doesn't Work

Cause: Using history mode without proper configuration.

Solution: Switch to hash routing:

  • React: HashRouter
  • Vue: createWebHashHistory()
  • Angular: HashLocationStrategy
  • SvelteKit: Configure fallback

Issue: Environment Variables Not Working

Cause: Build-time variables not being replaced.

Solution: Use framework-specific env variable patterns:

  • Next.js: NEXT_PUBLIC_
  • Vite: VITE_
  • Create React App: REACT_APP_
  • Angular: environment.ts

Issue: API Calls Fail on Device

Cause: CORS or localhost URLs.

Solution:

  1. Use production API URLs
  2. Configure CORS on backend
  3. Use Capacitor HTTP plugin for native requests:
import { CapacitorHttp } from '@capacitor/core';

const response = await CapacitorHttp.get({
  url: 'https://api.example.com/data',
});

Framework-Specific Plugins

Ionic Framework provides native UI components:

  • @ionic/react - React components
  • @ionic/vue - Vue components
  • @ionic/angular - Angular components

Konsta UI for Tailwind CSS:

  • Works with React, Vue, Svelte
  • iOS and Material Design themes

See ionic-design and konsta-ui skills for details.


Deployment Checklist

  • Configure static export (Next.js: output: 'export')
  • Set correct webDir in capacitor.config.ts
  • Use hash routing for mobile
  • Disable image optimization (Next.js)
  • Remove SSR/API routes dependencies
  • Add native permissions (Info.plist, AndroidManifest.xml)
  • Test on physical devices
  • Configure splash screen and icons
  • Set up live updates with Capgo (optional)
  • Build and test on iOS and Android

Resources


Framework-Specific Guides

For detailed guides on specific frameworks:


Next Steps

  1. Choose your framework and follow the setup above
  2. Configure static export/build
  3. Install and configure Capacitor
  4. Add platforms (iOS/Android)
  5. Build and sync
  6. Test on devices
  7. Add native features with plugins
  8. Set up live updates with Capgo
Weekly Installs
17
First Seen
7 days ago
Installed on
opencode17
gemini-cli17
codex17
github-copilot16
amp15
kimi-cli15