Nuxt standards
PWA de fidélité pour commerces, 100% offline, sans backend, sans compte. Deux modes : Commerçant (gestion des cartes et scan client) et Client (QR code personnel).
npx -y skills add chawkitariq/fidely --skill nuxt-standardsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.md
11.7 KB, as published. Nobody here has run it
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]