Aller au contenu

5 min de lecture

Nuxt et Capacitor, un seul code pour le web et les stores

Comment je transforme un site Nuxt en application iPhone et Android sans tout réécrire, ce qui change vraiment dans le projet, et les pièges que j’évite maintenant.

, développeuse front-end

  • Nuxt
  • Capacitor
  • Mobile

Une application mobile n’est pas toujours un nouveau projet. Quand le site existe déjà en Vue et Nuxt, Capacitor l’emballe dans une coque native et lui ouvre le téléphone : caméra, notifications, partage, stockage hors ligne. Le code reste le mien, les composants restent les mêmes, et l’application se publie sur l’App Store et Google Play.

Ce n’est pas magique pour autant. Voici ce que ça demande, étape par étape, et ce que j’ai appris en le faisant plusieurs fois.

Comment ça marche

Capacitor crée un projet iOS et un projet Android qui contiennent chacun une vue web plein écran. Cette vue charge les fichiers de votre site, embarqués dans l’application. Quand le code appelle une fonction native, Capacitor fait le pont avec Swift ou Kotlin, et renvoie le résultat en JavaScript.

Autrement dit, l’application est votre site, servi depuis le téléphone plutôt que depuis un serveur, avec une porte vers le matériel. Pas de serveur dans la poche de l’utilisateur, pas de réécriture.

Ce qui change dans le projet

Le rendu. L’application embarque une version statique du site. Dans Nuxt, je désactive le rendu côté serveur et je génère les fichiers :

nuxt.config.ts
export default defineNuxtConfig({
  // L’app tourne sans serveur : tout est rendu dans la vue web
  ssr: false,
})

Puis Capacitor pointe vers le dossier produit :

capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli'

const config: CapacitorConfig = {
  appId: 'fr.melaniedasilva.trajets',
  appName: 'Trajets',
  webDir: '.output/public',
}

export default config

Le cycle de travail tient en trois commandes : nuxt generate pour produire le site, npx cap sync pour le copier dans les projets natifs, npx cap open ios ou npx cap open android pour lancer Xcode ou Android Studio.

Les accès natifs. Chaque fonctionnalité du téléphone passe par un plugin, avec une version de repli sur le web. Le partage, par exemple :

composables/useShare.ts
import { Capacitor } from '@capacitor/core'
import { Share } from '@capacitor/share'

export function useShare() {
  return async (title: string, url: string) => {
    if (Capacitor.isNativePlatform()) {
      await Share.share({ title, url })
      return
    }
    // Sur le web : la boîte de partage du navigateur, ou le presse-papiers
    if (navigator.share) await navigator.share({ title, url })
    else await navigator.clipboard.writeText(url)
  }
}

Le composant qui appelle useShare ne sait pas où il tourne. C’est le but : un seul code, deux comportements.

La navigation. Les liens restent des liens, mais les gestes doivent répondre comme dans une application. Sur Android, le bouton retour du système doit revenir à la page précédente, pas fermer l’application :

plugins/back-button.client.ts
import { App } from '@capacitor/app'

export default defineNuxtPlugin(() => {
  const router = useRouter()
  App.addListener('backButton', ({ canGoBack }) => {
    if (canGoBack && router.currentRoute.value.path !== '/') router.back()
    else App.exitApp()
  })
})

Les pièges que j’évite maintenant

Le 100vh qui ment. Sur iOS, la barre d’adresse change la hauteur de la fenêtre, et l’encoche mange le haut de l’écran. J’utilise 100dvh et les marges de sécurité :

main.css
.screen {
  min-height: 100dvh;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}

Sans viewport-fit=cover dans la balise viewport, ces marges valent zéro. C’est la première chose que je vérifie quand « rien ne passe sous l’encoche » ne marche pas.

Le survol qui colle. Sur un écran tactile, :hover reste actif après le toucher. Un bouton garde sa couleur de survol jusqu’au prochain appui, et l’interface paraît cassée. Les états de survol sont réservés aux pointeurs fins :

main.css
@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translateY(-2px); }
}

Le zoom au focus. Un champ dont le texte fait moins de 16 px fait zoomer Safari quand on le touche. Taille minimale, toujours, même pour un petit champ de recherche.

La barre d’état invisible. Texte blanc sur fond blanc en haut de l’écran, parce que la barre d’état garde son style par défaut. Le plugin @capacitor/status-bar règle sa couleur selon le thème de l’application, et je le fais au démarrage, pas page par page.

Le réseau qui n’existe pas. Sur le web, une requête qui échoue affiche une erreur et on recharge. Dans une application, l’utilisateur est dans le métro. Chaque écran a un état hors ligne, et les données déjà chargées restent affichées. C’est le même principe que pour les états d’un écran, en plus strict.

Tester sur un vrai téléphone

Le simulateur suffit pour la mise en page. Il ne dit rien du toucher, de la latence, de la caméra ni du clavier qui recouvre la moitié de l’écran. Avant chaque version, l’application tourne sur un iPhone et un Android que je garde pour ça.

Mettre à jour

Le code web se met à jour comme un site : je génère, je synchronise, je publie une nouvelle version dans les stores. La relecture d’Apple prend de quelques heures à quelques jours, celle de Google est en général plus rapide. Un bug visible le lundi est corrigé en ligne le lundi, et dans les stores un peu plus tard. Je le dis au client dès le début, pour que personne ne l’apprenne le jour du bug.

Quand ça vaut le coup

Quand l’application est l’usage principal, pas une vitrine. Un tableau de bord qu’on ouvre tous les jours, un outil de terrain qui prend des photos, un espace client avec des notifications. Là, la présence sur les stores et l’accès au matériel changent vraiment l’usage.

Pour un site vitrine, une page web rapide, installable et bien référencée suffit souvent. Je le dis quand c’est le cas, parce qu’une application à maintenir sans raison coûte plus qu’elle ne rapporte.

Dans tous les cas, c’est le même code que je teste, que je publie et que je fais évoluer. Une seule base, deux magasins.

Un projet, une question ?

Un projet, une question ? Écrivez-moi, je réponds sous 48 h.

contact@melaniedasilva.fr

À lire ensuite

    • IA
    • Méthode
    • Claude Code

    Coder avec une IA sans perdre la main

    J’utilise Claude Code tous les jours. Voici ce que je lui confie, ce que je garde, et les règles qui m’évitent de livrer du code que je ne comprends pas.

    4 min de lecture

    • IA
    • Interface
    • Accessibilité

    Ce qu’une IA ne voit pas dans une interface

    Une IA génère un écran correct en quelques secondes. Les détails qui le rendent agréable, ou simplement utilisable, viennent après. Liste de ce que je vérifie à la main.

    4 min de lecture