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— 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 :
camelCasepréfixéuse(ex:useDB.ts,useClient.ts) - Pages :
kebab-case.vueou[param].vuepour 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.vueouapp.config.ts - Jamais de composant dans
pages/— extraire danscomponents/ 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
<!-- app/layouts/default.vue -->
<template>
<div class="min-h-screen flex flex-col">
<slot /> <!-- NuxtPage s'insère ici -->
</div>
</template>
<!-- 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
<!-- 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é aveclayout: 'nom-kebab' default.vues'applique automatiquement à toutes les pages sansdefinePageMeta- 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éreruseSeoMetapage 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é)
// 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
})
<!-- Activer un middleware sur une page -->
<script setup lang="ts">
definePageMeta({ middleware: 'mode' })
</script>
3. Pages (app/pages/)
<!-- ✅ 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 appelerindexedDBdirectement 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 |
<!-- ✅ 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-standardspour 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
<!-- ✅ 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
// ✅ 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
<!-- ✅ 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
<!-- ✅ bouton avec état de chargement -->
<UButton
color="primary"
:loading="isSaving"
:disabled="isSaving"
@click="save"
>
Enregistrer
</UButton>
9. Routing Nuxt
// 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
watchinutile — préférercomputedquand la valeur est dérivée de props/state - Cleanup obligatoire dans
onBeforeUnmountpour les listeners, timers, et instances browser (ex:scanner.clear()) - Dynamic imports pour les libs lourdes uniquement chargées côté client :
const { Html5QrcodeScanner } = await import('html5-qrcode') const QRCode = await import('qrcode') - Pas de
v-forsans:key— la clé doit être un identifiant stable (l'idde l'objet, jamais l'index) v-htmlinterdit 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]