# Nuxt Standards

> Structure fichiers et conventions Nuxt 4 pour Fidely (dossier app/, pages/, components/, composables/). Déclencher quand : ajout/déplacement de fichier, création de layout, page Nuxt, SSR safety, routing (navigateTo, useRoute), pattern ClientOnly, dynamic import html5-qrcode/qrcode, performance. Mots-clés : app/, pages/, layouts/, components/, useDB, SSR, onMounted, ClientOnly, NuxtLink, navigateTo, definePageMeta, html5-qrcode, qrcode, onBeforeUnmount, v-for :key, v-html, UModal, UForm.

- Skill: `chawkitariq/nuxt-standards` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add chawkitariq/nuxt-standards`
- Raw SKILL.md: https://api.skillmd.com/api/skills/chawkitariq/nuxt-standards/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: chawkitariq (https://skillmd.com/u/chawkitariq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/chawkitariq/nuxt-standards

---


# Skill : Standards & bonnes pratiques Nuxt 3/4

Appliquer ces règles lors de toute création ou modification de code dans le projet.

## Ressources disponibles

- [`reference/ssr-patterns.md`](reference/ssr-patterns.md) — dynamic imports, ClientOnly, guards SSR, routes Fidely, pattern UModal suppression

---

## 1. Structure des fichiers (Nuxt 4 — dossier `app/`)

```
app/
├── app.vue              # Racine unique — uniquement <UApp> + <NuxtPage /> + useHead/useSeoMeta
├── app.config.ts        # Thème UI uniquement — ne pas y mettre de logique
├── assets/              # CSS, fonts, images importées par Vite
├── components/          # PascalCase.vue — auto-importés
├── composables/         # useXxx.ts — auto-importés
├── layouts/             # default.vue + layouts nommés — wrappent <NuxtPage />
├── pages/               # kebab-case.vue ou [param].vue — routing automatique
└── utils/               # camelCase.ts — auto-importés
```

**Conventions de nommage :**
- Composants : `PascalCase.vue` (ex: `CardItem.vue`)
- Composables : `camelCase` préfixé `use` (ex: `useDB.ts`, `useClient.ts`)
- Pages : `kebab-case.vue` ou `[param].vue` pour les routes dynamiques
- Utils : `camelCase.ts`
- Icônes : toujours `i-lucide-*` (Lucide en priorité)

**Règles absolues :**
- Un fichier = une responsabilité
- Jamais de logique métier dans `app.vue` ou `app.config.ts`
- Jamais de composant dans `pages/` — extraire dans `components/`
- `server/` uniquement si une API route SSR est délibérément ajoutée

---

## 1b. TypeScript, ESLint, JSDoc, structure composants & composables

> Ces règles sont centralisées dans le skill **`coding-standards`** — s'y référer pour :
> - TypeScript strict (zéro `any`, `import type`, retours typés)
> - ESLint / formatage
> - JSDoc obligatoire sur toutes les fonctions et interfaces exportées
> - Structure `<script setup>` et ordre des blocs
> - Conventions composables (`useXxx`, guards SSR, auto-imports)

---

## 2. Layouts (`app/layouts/`)

### Quand utiliser un layout

| Situation | Solution |
|---|---|
| Toutes les pages partagent le même chrome (header, footer, nav) | `layouts/default.vue` |
| Certaines pages ont un chrome différent (ex: plein écran vs avec nav) | Layout nommé (`layouts/fullscreen.vue`) |
| Une seule page a un comportement unique | `definePageMeta` sur la page uniquement |
| App simple sans chrome global répété | Pas de layout — `app.vue` suffit |

> **Projet Fidely** : pas de layout actuellement (`app.vue` = `<UApp><NuxtPage /></UApp>`). Créer un layout si du chrome commun (ex: bottom nav, header Pro) doit apparaître sur plusieurs pages.

### Créer un layout

```vue
<!-- app/layouts/default.vue -->
<template>
  <div class="min-h-screen flex flex-col">
    <slot />   <!-- NuxtPage s'insère ici -->
  </div>
</template>
```

```vue
<!-- app/layouts/merchant.vue — layout pour les pages Pro -->
<script setup lang="ts">
function switchToClient() {
  localStorage.setItem('fidely_mode', 'client')
  navigateTo('/me')
}
</script>

<template>
  <div class="min-h-screen flex flex-col">
    <!-- Header commun à toutes les pages Pro -->
    <div class="flex items-center justify-between px-4 py-4 border-b border-default">
      <h1 class="text-2xl font-bold text-primary">Fidely Pro</h1>
      <UButton
        icon="i-lucide-user-circle"
        color="neutral"
        variant="ghost"
        size="lg"
        @click="switchToClient"
      />
    </div>
    <slot />
  </div>
</template>
```

### Activer un layout dans une page

```vue
<!-- Méthode 1 : definePageMeta (recommandée — statique, analysable) -->
<script setup lang="ts">
definePageMeta({ layout: 'merchant' })
</script>

<!-- Méthode 2 : layout dynamique (si le layout dépend d'une condition runtime) -->
<script setup lang="ts">
const { layout } = useRoute().meta  // récupéré depuis un middleware si besoin
</script>
<template>
  <NuxtLayout name="merchant">
    <!-- contenu -->
  </NuxtLayout>
</template>

<!-- Méthode 3 : désactiver le layout default sur une page spécifique -->
<script setup lang="ts">
definePageMeta({ layout: false })
</script>
```

### Règles layouts

- **Nommage** : `kebab-case.vue` → activé avec `layout: 'nom-kebab'`
- **`default.vue`** s'applique automatiquement à toutes les pages sans `definePageMeta`
- **Ne pas dupliquer** la logique de switch de mode dans chaque page si un layout peut la centraliser
- **`<slot />`** est obligatoire — sans lui, le contenu de la page n'est pas rendu
- **Pas de logique métier lourde** dans un layout — uniquement chrome UI + navigation
- **`useHead()` dans le layout** : possible pour les meta communes, mais préférer `useSeoMeta` page par page pour la flexibilité
- **Layouts et `<ClientOnly>`** : si le layout contient des composants browser-only, les wrapper dans `<ClientOnly>`

### Middleware de layout (cas avancé)

```ts
// app/middleware/mode.ts — redirige selon le mode mémorisé
export default defineNuxtRouteMiddleware(() => {
  if (import.meta.server) return
  const mode = localStorage.getItem('fidely_mode')
  // logique de redirection
})
```

```vue
<!-- Activer un middleware sur une page -->
<script setup lang="ts">
definePageMeta({ middleware: 'mode' })
</script>
```

---

## 3. Pages (`app/pages/`)


```vue
<!-- ✅ Correct -->
<script setup lang="ts">
const route = useRoute()
const toast = useToast()
const { getAllCards } = useDB()   // logique dans composable

const cards = ref<Card[]>([])

onMounted(async () => {
  cards.value = await getAllCards()
})
</script>

<template>
  <div class="min-h-screen flex flex-col">
    <!-- contenu -->
  </div>
</template>
```

**Ce qu'une page NE doit PAS contenir :**
- Logique IndexedDB directe (→ composable)
- Logique de validation (→ `utils/validators.ts`)
- Composants inline définis dans `<script>` (→ `components/`)
- Plus de ~150 lignes de template (→ découper en composants)

---

## 4. Composants (`app/components/`)

> Structure détaillée dans **`coding-standards`** (ordre des blocs, props, emits, computed, JSDoc).

**Ce qu'un composant NE doit PAS contenir :**
- Logique IndexedDB directe (→ composable)
- Logique de validation (→ `utils/validators.ts`)
- Composants inline définis dans `<script>` (→ `components/`)
- Plus de ~150 lignes de template (→ découper en composants)

---

## 5. Composables (`app/composables/`)

> Pattern complet, JSDoc et guards SSR dans **`coding-standards`**.

- `useDB()` est le seul point d'entrée pour IndexedDB — ne jamais appeler `indexedDB` directement dans une page ou un composant

---

## 6. SSR Safety — règles strictes

| API | Règle |
|---|---|
| `indexedDB` | Toujours dans `onMounted` ou derrière `if (import.meta.server) return` |
| `localStorage` | Toujours dans `onMounted` — jamais au top-level du `<script setup>` |
| `document` / `window` | Toujours dans `onMounted` ou `<ClientOnly>` |
| `html5-qrcode` | `await import('html5-qrcode')` dans `onMounted` uniquement |
| `qrcode` | `await import('qrcode')` dans `onMounted` + composant dans `<ClientOnly>` |
| `crypto.randomUUID()` / `uuidv7()` | Sûr côté serveur et client |

```vue
<!-- ✅ ClientOnly pour les composants browser-only -->
<ClientOnly>
  <Scanner @scan="onScan" />
  <template #fallback>
    <div class="h-64 bg-gray-100 animate-pulse rounded-xl" />
  </template>
</ClientOnly>
```

---

## 7. Auto-imports Nuxt — ne jamais importer manuellement

> Voir **`coding-standards`** pour les règles complètes.

**Auto-importés :** `ref`, `computed`, `reactive`, `watch`, `watchEffect`, `onMounted`, `onBeforeUnmount`, `useRoute`, `useRouter`, `navigateTo`, `useToast`, `useHead`, `useSeoMeta`, et tous les composables/utils du dossier `app/`

---

## 8. Nuxt UI v4 — patterns corrects

### Formulaires
```vue
<!-- ✅ UForm avec validate function (Zod) -->
<UForm :state="state" :validate="validate" @submit="onSubmit">
  <UFormField name="title" label="Titre">   <!-- name = clé dans state -->
    <UInput v-model="state.title" size="lg" class="w-full" />
  </UFormField>
  <UButton type="submit" color="primary" :loading="isSaving">
    Enregistrer
  </UButton>
</UForm>
```

### Toasts — couleurs valides uniquement
```ts
// ✅ couleurs valides
toast.add({ title: 'OK', color: 'success' })
toast.add({ title: 'Erreur', color: 'error' })
toast.add({ title: 'Info', color: 'primary' })
// ❌ ne pas utiliser 'green', 'red', 'blue' — tomberont en 'neutral'
```

### Modales
```vue
<!-- ✅ v-model:open (pas v-model) -->
<UModal v-model:open="isOpen" title="Confirmation">
  <template #body>Voulez-vous supprimer cette carte ?</template>
  <template #footer>
    <UButton color="neutral" variant="outline" @click="isOpen = false">Annuler</UButton>
    <UButton color="error" :loading="isDeleting" @click="confirm">Supprimer</UButton>
  </template>
</UModal>
```

### Loading states — obligatoire sur tout bouton async
```vue
<!-- ✅ bouton avec état de chargement -->
<UButton
  color="primary"
  :loading="isSaving"
  :disabled="isSaving"
  @click="save"
>
  Enregistrer
</UButton>
```

---

## 9. Routing Nuxt

```ts
// Navigation programmatique
await navigateTo('/merchant')              // ✅ helper Nuxt auto-importé
await navigateTo(`/client/${clientId}`)   // ✅ route dynamique

// Lecture des params
const route = useRoute()
const id = route.params.id as string      // ✅ typer explicitement

// Liens dans les templates
<NuxtLink to="/merchant">Retour</NuxtLink>  // ✅ préférer aux <a>
<UButton to="/scan">Scanner</UButton>       // ✅ UButton supporte la prop to
```

---

## 10. Performance & bonnes pratiques

- **Pas de `watch` inutile** — préférer `computed` quand la valeur est dérivée de props/state
- **Cleanup obligatoire** dans `onBeforeUnmount` pour les listeners, timers, et instances browser (ex: `scanner.clear()`)
- **Dynamic imports** pour les libs lourdes uniquement chargées côté client :
  ```ts
  const { Html5QrcodeScanner } = await import('html5-qrcode')
  const QRCode = await import('qrcode')
  ```
- **Pas de `v-for` sans `:key`** — la clé doit être un identifiant stable (l'`id` de l'objet, jamais l'index)
- **`v-html` interdit** sur du contenu utilisateur (XSS)

---

## 11. Checklist avant de marquer une tâche ✅

> Checklist complète (TypeScript, JSDoc, ESLint, tests) dans **`coding-standards`**.

**Points Nuxt spécifiques :**
```
[ ] Guards SSR en place sur toutes les browser APIs (import.meta.server)
[ ] Composable useDB() utilisé pour tout accès IndexedDB (jamais indexedDB direct)
[ ] Loading states sur tous les boutons async (:loading + :disabled)
[ ] Confirmation UModal avant toute action destructive
[ ] Validation Zod avant toute persistance de formulaire
[ ] Icônes Lucide (i-lucide-*) utilisées en priorité
[ ] Pas de dark mode introduit (classes dark:, useColorMode)
[ ] Cleanup onBeforeUnmount si ressource browser ouverte (scanner.clear())
[ ] Dynamic imports pour html5-qrcode et qrcode dans onMounted uniquement
[ ] Pas de routeRules prerender — l'app dépend de IndexedDB/localStorage au montage
```

---

## Confirmation en fin de tâche

> ✅ **nuxt-standards appliqués**
> - Structure fichiers : [conforme app/]
> - SSR safety : [guards en place / ClientOnly utilisé]
> - Nuxt UI : [composants sémantiques utilisés]
> - Performance : [cleanup / dynamic imports]

