- name
- nextjs-capacitor
- description
- Project-agnostic guide for setting up Next.js with Capacitor for native mobile support and Ionic React for UI components. Includes core setup, optional enhancements, and complete push notifications implementation.
# Next.js + Capacitor + Ionic React Setup
## Purpose
This skill provides a comprehensive, project-agnostic guide for setting up a Next.js application with Capacitor for native mobile support and Ionic React for UI components. It covers core setup requirements, optional enhancements, and complete push notification implementation.
## When to Use
Use this skill when:
- Setting up a new Next.js project with Capacitor and Ionic React
- Adding Capacitor to an existing Next.js application
- Configuring push notifications for a Capacitor app
- Troubleshooting Capacitor build or sync issues
- Understanding the conditional static export pattern for Next.js + Capacitor
## Architecture Overview
### Project Structure Options
You can organize your project in two ways:
**Option A: Root-level `src/` directory** (Single app)
```
your-project/
├── src/ # Next.js frontend at root
│ └── app/
├── backend/ # Optional backend (if monorepo)
├── capacitor.config.ts
└── package.json
```
- Capacitor `webDir`: `"dist"`
- Common for single-app projects
**Option B: Separate `frontend/` directory** (Monorepo)
```
your-project/
├── frontend/ # Next.js frontend
│ └── src/
│ └── app/
├── backend/ # Backend API
├── capacitor.config.ts
└── package.json
```
- Capacitor `webDir`: `"frontend/dist"`
- Better for projects with separate frontend/backend
### Key Concepts
- **Conditional Static Export**: Next.js only exports statically when `CAPACITOR_BUILD=true`, allowing normal Next.js development
- **Capacitor Integration**: Capacitor wraps the static Next.js build into native iOS/Android apps
- **Ionic React**: Provides mobile-optimized UI components that work on web and native
## Core Setup Instructions
### Step 1: Create Next.js Project
```bash
npx create-next-app@latest . --typescript --app --tailwind --eslint --src-dir
```
When prompted:
- Choose **TypeScript** (recommended)
- Choose **App Router** (required)
- Choose **Tailwind CSS** (optional but recommended)
- Choose **ESLint** (recommended)
### Step 2: Install Core Dependencies
```bash
# Capacitor Core
npm install @capacitor/core @capacitor/cli @capacitor/android @capacitor/ios
# Ionic React
npm install @ionic/react ionicons
# Optional but recommended Capacitor plugins
npm install @capacitor/splash-screen @capacitor/status-bar @capacitor/app
```
### Step 3: Initialize Capacitor
```bash
npx cap init
```
When prompted:
- **App name**: YourAppName
- **App ID**: com.yourcompany.yourapp (use reverse domain notation)
- **Web dir**: `dist` (for root `src/`) or `frontend/dist` (for monorepo)
This creates `capacitor.config.ts` at the root level.
### Step 4: Configure Capacitor
Update `capacitor.config.ts`:
```typescript
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.yourcompany.yourapp",
appName: "YourAppName",
webDir: "dist", // or "frontend/dist" for monorepo
plugins: {
SplashScreen: {
launchAutoHide: false, // Control manually for better UX
},
StatusBar: {
style: "DARK",
overlaysWebView: false,
backgroundColor: "#000000",
},
},
};
export default config;
```
### Step 5: Configure Next.js
Update `next.config.js`:
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
// Only use static export when building for Capacitor
...(process.env.CAPACITOR_BUILD === "true" && {
output: "export",
images: {
unoptimized: true, // Required for static export
},
trailingSlash: true,
distDir: "dist", // Must match Capacitor's webDir
}),
transpilePackages: ["@ionic/react", "@ionic/core", "@stencil/core"],
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
webpack: (config, { isServer }) => {
// Handle Stencil dynamic imports and Node.js polyfills
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
crypto: false,
stream: false,
util: false,
path: false,
os: false,
tls: false,
net: false,
dns: false,
child_process: false,
http: false,
https: false,
zlib: false,
querystring: false,
url: false,
buffer: false,
timers: false,
"timers/promises": false,
diagnostics_channel: false,
};
}
// Ignore dynamic import warnings for Stencil
config.module = {
...config.module,
unknownContextCritical: false,
unknownContextRegExp: /^\.\/.*$/,
unknownContextRequest: ".",
};
return config;
},
};
module.exports = nextConfig;
```
**Important**: The conditional static export allows normal Next.js development while enabling Capacitor builds when needed.
### Step 6: Update TypeScript Config
Update `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next-env.d.ts",
"dist/types/**/*.ts"
],
"exclude": ["node_modules"]
}
```
### Step 7: Create Root Layout
Create `src/app/layout.tsx`:
```tsx
import type { Metadata } from "next";
import "@ionic/react/css/core.css";
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";
import "@ionic/react/css/ionic.bundle.css";
import "@ionic/react/css/palettes/dark.css";
import "./globals.css";
import IonicApp from "./IonicApp";
export const metadata: Metadata = {
title: "YourAppName",
description: "Your app description",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<meta
name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#31d53d" />
</head>
<body className="" style={{ overflow: "hidden" }}>
<IonicApp>{children}</IonicApp>
</body>
</html>
);
}
```
### Step 8: Create Basic IonicApp Component
Create `src/app/IonicApp.tsx`:
```tsx
"use client";
import { IonApp, setupIonicReact } from "@ionic/react";
setupIonicReact();
export default function IonicApp({ children }: { children: React.ReactNode }) {
return <IonApp>{children}</IonApp>;
}
```
### Step 9: Create Global Styles
Create `src/app/globals.css`:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--ion-color-primary: #31d53d;
--ion-color-primary-rgb: 49, 213, 61;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #2bbb36;
--ion-color-primary-tint: #46d954;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
```
### Step 10: Update Package Scripts
Update `package.json` scripts:
```json
{
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start",
"lint": "next lint",
"cap:sync": "CAPACITOR_BUILD=true next build && cap sync",
"cap:ios": "CAPACITOR_BUILD=true next build && cap sync ios",
"cap:android": "CAPACITOR_BUILD=true next build && cap sync android",
"cap:open:ios": "cap open ios",
"cap:open:android": "cap open android"
}
}
```
### Step 11: Add Native Platforms
```bash
# iOS
npx cap add ios
npx cap sync
# Android
npx cap add android
npx cap sync
```
## Optional Enhancements
### Enhanced IonicApp Component
You can enhance the basic `IonicApp.tsx` with additional features:
```tsx
"use client";
import { useEffect } from "react";
import { IonApp, setupIonicReact } from "@ionic/react";
import { Capacitor } from "@capacitor/core";
import { SplashScreen } from "@capacitor/splash-screen";
import { StatusBar, Style } from "@capacitor/status-bar";
import { App, AppState } from "@capacitor/app";
setupIonicReact();
export default function IonicApp({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Add platform class to body for CSS targeting
if (Capacitor.isNativePlatform()) {
document.body.classList.add('native-platform');
} else {
document.body.classList.add('web-platform');
// Add debug safe area indicators for Chrome DevTools testing
document.documentElement.style.setProperty('--safe-area-inset-top', '44px');
document.documentElement.style.setProperty('--safe-area-inset-bottom', '34px');
}
// Configure StatusBar on native platforms
if (Capacitor.isNativePlatform()) {
try {
StatusBar.setStyle({ style: Style.Dark });
StatusBar.setOverlaysWebView({ overlay: false });
StatusBar.setBackgroundColor({ color: "#000000" });
} catch (error) {
console.warn("Failed to configure status bar:", error);
}
try {
// Hide splash screen after app loads
setTimeout(() => {
SplashScreen.hide({ fadeOutDuration: 200 });
}, 100);
} catch (error) {
console.warn("Failed to hide splash screen:", error);
}
// Listen for app state changes
const stateListener = App.addListener("appStateChange", (state: AppState) => {
if (state.isActive) {
// Handle app becoming active
console.log("App became active");
}
});
return () => {
document.body.classList.remove('native-platform', 'web-platform');
stateListener.remove();
};
}
}, []);
return <IonApp>{children}</IonApp>;
}
GitHub에서 보기