← Volver al blog
Giancarlos Gaona·Publicado el 17 de marzo de 2026 · Actualizado el 3 de agosto de 2026·8 min de lectura

Crear una app móvil con Next.js y Capacitor: de web a nativa

Next.jsMobileCapacitor

Por qué Next.js y Capacitor

Cuando necesitas una app móvil pero tu equipo domina tecnologías web, las opciones clásicas son React Native o Flutter. Ambas requieren aprender paradigmas nuevos: componentes nativos en lugar de HTML, estilos propios en lugar de CSS, y herramientas de build completamente distintas. Hay una tercera vía que se discute menos: tomar una aplicación web existente y empaquetarla como app nativa con acceso a APIs del dispositivo.

Capacitor, creado por el equipo de Ionic, hace exactamente esto. A diferencia de Cordova (su predecesor), Capacitor trata el proyecto nativo como un artefacto de primera clase: genera proyectos reales de Android Studio y Xcode que puedes modificar directamente. No es un WebView opaco; es un proyecto nativo con un WebView como capa de presentación.

La combinación con Next.js es particularmente interesante cuando usas output: "export" para generar un sitio completamente estático. Capacitor toma esos archivos HTML, CSS y JavaScript generados y los sirve desde el WebView nativo. El resultado es una app que se siente rápida porque los assets son locales (no hay descarga de red) y tiene acceso a Geolocation, Camera, Filesystem y docenas de APIs nativas.

Esta es la arquitectura que usé para construir MiFarmaciApp, una aplicación de farmacias de turno en Chile que funciona como web progresiva y como app nativa en Android e iOS, compartiendo el 100% del código de la interfaz.

Configuración inicial

Partimos de un proyecto Next.js existente. El requisito fundamental es que soporte exportación estática:

// next.config.ts
const nextConfig = {
  output: "export",
  images: {
    unoptimized: true,
  },
};

export default nextConfig;

La propiedad images.unoptimized es necesaria porque el optimizador de imágenes de Next.js requiere un servidor Node.js, que no existe en el contexto de Capacitor.

Instalamos Capacitor y sus dependencias:

npm install @capacitor/core @capacitor/cli
npx cap init "MiApp" "com.ejemplo.miapp" --web-dir out

El parámetro --web-dir out es clave: le dice a Capacitor que los archivos web están en el directorio out/, que es donde Next.js genera la exportación estática.

Ahora agregamos las plataformas nativas:

npm install @capacitor/android @capacitor/ios
npx cap add android
npx cap add ios

Esto genera los directorios android/ e ios/ con proyectos nativos completos. Estos directorios deben incluirse en el repositorio, a diferencia de Cordova donde se generaban cada vez.

Configuración de capacitor.config.ts

El archivo de configuración central controla el comportamiento del WebView y las integraciones nativas:

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

const config: CapacitorConfig = {
  appId: "com.ejemplo.miapp",
  appName: "MiApp",
  webDir: "out",
  server: {
    androidScheme: "https",
    iosScheme: "https",
  },
  plugins: {
    SplashScreen: {
      launchAutoHide: false,
      backgroundColor: "#0e0d0c",
      androidScaleType: "CENTER_CROP",
      showSpinner: false,
    },
    StatusBar: {
      style: "DARK",
      backgroundColor: "#0e0d0c",
    },
  },
};

export default config;

La propiedad androidScheme: "https" es importante para que las APIs web que requieren contexto seguro (como Geolocation) funcionen correctamente dentro del WebView. Sin esto, el esquema por defecto es http y ciertas APIs del navegador se bloquean.

Plugins nativos

La verdadera ventaja de Capacitor sobre una PWA es el acceso a APIs nativas mediante plugins. Cada plugin tiene una interfaz TypeScript idéntica en ambas plataformas.

Geolocation

npm install @capacitor/geolocation
npx cap sync
import { Geolocation } from "@capacitor/geolocation";

async function obtenerUbicacion() {
  const permisos = await Geolocation.checkPermissions();

  if (permisos.location !== "granted") {
    await Geolocation.requestPermissions();
  }

  const posicion = await Geolocation.getCurrentPosition({
    enableHighAccuracy: true,
    timeout: 10000,
  });

  return {
    lat: posicion.coords.latitude,
    lng: posicion.coords.longitude,
  };
}

En Android, necesitas agregar el permiso en android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Camera

npm install @capacitor/camera
npx cap sync
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";

async function tomarFoto() {
  const imagen = await Camera.getPhoto({
    quality: 80,
    allowEditing: false,
    resultType: CameraResultType.Base64,
    source: CameraSource.Camera,
  });

  return `data:image/${imagen.format};base64,${imagen.base64String}`;
}

SplashScreen

El splash screen se controla programáticamente, permitiendo ocultarlo cuando la app está lista:

import { SplashScreen } from "@capacitor/splash-screen";

// En el componente raiz de la app
useEffect(() => {
  const inicializar = async () => {
    await cargarDatosIniciales();
    await SplashScreen.hide({ fadeOutDuration: 300 });
  };

  inicializar();
}, []);

Detectar la plataforma y adaptar la UI

No toda la interfaz web funciona igual en un contexto móvil. Los gestos son distintos, las áreas seguras (notch, barra de navegación) ocupan espacio, y ciertas interacciones deben adaptarse.

Capacitor expone la plataforma actual:

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

const esNativa = Capacitor.isNativePlatform();
const plataforma = Capacitor.getPlatform(); // 'android', 'ios', 'web'

Un hook personalizado facilita la adaptación:

import { useState, useEffect } from "react";
import { Capacitor } from "@capacitor/core";

export function usePlataforma() {
  const [info, setInfo] = useState({
    esNativa: false,
    plataforma: "web" as "android" | "ios" | "web",
  });

  useEffect(() => {
    setInfo({
      esNativa: Capacitor.isNativePlatform(),
      plataforma: Capacitor.getPlatform() as "android" | "ios" | "web",
    });
  }, []);

  return info;
}

Para las áreas seguras en iOS (notch del iPhone), usamos las variables CSS nativas del WebView:

.header {
  padding-top: env(safe-area-inset-top);
}

.tab-bar {
  padding-bottom: env(safe-area-inset-bottom);
}

Y en el index.html generado, el viewport debe incluir viewport-fit=cover:

<meta
  name="viewport"
  content="width=device-width, initial-scale=1, viewport-fit=cover"
/>

Build para Android

El flujo de build para Android requiere tener instalado Android Studio con el SDK correspondiente.

# Generar la exportacion estatica de Next.js
npm run build

# Copiar los archivos web al proyecto nativo
npx cap sync android

# Abrir en Android Studio
npx cap open android

Desde Android Studio puedes ejecutar en un emulador o dispositivo físico. Para generar un APK de distribución:

  1. Menú Build > Generate Signed Bundle / APK
  2. Seleccionar APK o Android App Bundle (AAB, requerido para Google Play)
  3. Crear o seleccionar un keystore para firmar la app
  4. Elegir el build variant release

También puedes generar el APK desde la línea de comandos:

cd android
./gradlew assembleRelease

El APK resultante estará en android/app/build/outputs/apk/release/.

Para automatizar esto en CI, un workflow de GitHub Actions sería:

- name: Build Next.js
  run: npm run build

- name: Sync Capacitor
  run: npx cap sync android

- name: Build APK
  working-directory: android
  run: ./gradlew assembleRelease

- name: Upload APK
  uses: actions/upload-artifact@v4
  with:
    name: app-release.apk
    path: android/app/build/outputs/apk/release/app-release-unsigned.apk

Build para iOS

iOS requiere macOS con Xcode instalado. No es posible compilar para iOS desde Windows o Linux.

npm run build
npx cap sync ios
npx cap open ios

Esto abre el proyecto en Xcode. Los pasos adicionales para iOS son:

  1. Seleccionar el Signing Team en la pestaña Signing & Capabilities del target
  2. Configurar el Bundle Identifier (debe coincidir con el appId de Capacitor)
  3. Para distribución, crear un provisioning profile en el Apple Developer Portal

Para generar un IPA para TestFlight o el App Store:

  1. Menú Product > Archive
  2. En el Organizer, seleccionar Distribute App
  3. Elegir App Store Connect o Ad Hoc según el destino

La firma de código en iOS es significativamente más compleja que en Android. Necesitas un Apple Developer Program ($99/anual), certificados de distribución y provisioning profiles que vinculen tu app con dispositivos específicos (en desarrollo) o con el App Store (en producción).

Live reload durante desarrollo

Uno de los mayores beneficios de este enfoque es poder desarrollar la UI en el navegador y solo probar en el dispositivo cuando trabajas con plugins nativos.

Para live reload en el dispositivo, Capacitor puede apuntar a tu servidor de desarrollo:

// capacitor.config.ts (solo para desarrollo)
const config: CapacitorConfig = {
  // ...
  server: {
    url: "http://192.168.1.50:3000", // IP local de tu maquina
    cleartext: true,
  },
};

Con esto, la app en el dispositivo carga la UI desde tu servidor de desarrollo y los cambios se reflejan al instante. Los plugins nativos siguen funcionando porque el bridge de Capacitor opera independientemente del origen del contenido web.

Recuerda revertir esta configuración antes de hacer un build de producción. Una buena práctica es usar variables de entorno para alternar entre modos.

Problemas comunes y soluciones

Navegación con rutas: Next.js con exportación estática genera archivos como about.html. Si usas <Link href="/about">, el WebView buscará /about sin extensión. La solución es configurar trailingSlash: true en next.config.ts, que genera about/index.html y la navegación funciona correctamente.

CORS en peticiones API: Desde el WebView nativo, las peticiones van al servidor API sin el header Origin habitual. Configura tu API para aceptar peticiones desde el scheme de Capacitor (capacitor://localhost en iOS, https://localhost en Android).

Rendimiento del WebView: En dispositivos Android de gama baja, el WebView puede ser lento. Minimiza las animaciones CSS complejas, usa will-change con moderación, y prefiere transform y opacity para transiciones que se ejecutan en la GPU.

Teclado virtual en Android: El teclado puede cubrir inputs. Agrega android:windowSoftInputMode="adjustResize" en el AndroidManifest.xml dentro de la etiqueta <activity> para que el WebView se redimensione automáticamente.

Conclusiones

Capacitor con Next.js no reemplaza a React Native o Flutter para aplicaciones con interfaces completamente nativas. Pero para aplicaciones donde la UI web es suficiente y necesitas acceso a APIs del dispositivo, es una solución pragmática que elimina la necesidad de mantener dos codebases. El mismo equipo frontend que construye la web puede entregar apps nativas, y el mismo código funciona en tres plataformas con adaptaciones mínimas.

GG
Giancarlos Gaona

Ingeniero de Software y Consultor Senior especializado en Liferay DXP, con más de 8 años de experiencia liderando proyectos enterprise para banca, retail, energía e instituciones públicas.