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.
Appliquer ces règles lors de toute création ou modification de code dans le projet.
reference/ssr-patterns.md — dynamic imports, ClientOnly, guards SSR, routes Fidely, pattern UModal suppressionapp/)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 :
PascalCase.vue (ex: CardItem.vue)camelCase préfixé use (ex: useDB.ts, useClient.ts)kebab-case.vue ou [param].vue pour les routes dynamiquescamelCase.tsi-lucide-* (Lucide en priorité)Règles absolues :
app.vue ou app.config.tspages/ — extraire dans components/server/ uniquement si une API route SSR est délibérément ajoutéeCes 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)
app/layouts/)| 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.
<!-- 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>
<!-- 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>
kebab-case.vue → activé avec layout: 'nom-kebab'default.vue s'applique automatiquement à toutes les pages sans definePageMeta<slot /> est obligatoire — sans lui, le contenu de la page n'est pas renduuseHead() dans le layout : possible pour les meta communes, mais préférer useSeoMeta page par page pour la flexibilité<ClientOnly> : si le layout contient des composants browser-only, les wrapper dans <ClientOnly>// 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>
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 :
utils/validators.ts)<script> (→ components/)app/components/)Structure détaillée dans
coding-standards(ordre des blocs, props, emits, computed, JSDoc).
Ce qu'un composant NE doit PAS contenir :
utils/validators.ts)<script> (→ components/)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| 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>
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/
<!-- ✅ 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>
// ✅ 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'
<!-- ✅ 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>
<!-- ✅ bouton avec état de chargement -->
<UButton
color="primary"
:loading="isSaving"
:disabled="isSaving"
@click="save"
>
Enregistrer
</UButton>
// 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
watch inutile — préférer computed quand la valeur est dérivée de props/stateonBeforeUnmount pour les listeners, timers, et instances browser (ex: scanner.clear())const { Html5QrcodeScanner } = await import('html5-qrcode')
const QRCode = await import('qrcode')
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)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
✅ 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]