Architecture
Co-localisation des composants, groupes de routes, proxy et lib.
Le projet suit une règle stricte : les composants vivent au plus proche
de la page qui les utilise. Seul ce qui est réellement partagé entre
plusieurs pages remonte dans components/.
Co-localisation
components/ — global uniquement
components/
├── ui/ # Primitives shadcn (button, card, dialog, switch...)
│ # + DataList générique
├── layout/ # Header, Footer, AdminSidebar/Header/Shell, UserSidebar/Header/Shell
├── providers/ # ThemeProvider, NextIntlProvider
├── shared/ # ThemeToggle, LocaleSwitcher, AnnouncementBanner, ImpersonationBanner
└── user/ # UserAvatar, UserMenu, UserDropdown_components/ — co-localisé
Chaque page (ou groupe de pages) a son _components/ à côté. Le préfixe
_ empêche Next.js de créer une route à partir du dossier.
app/[locale]/(public)/_components/
hero.tsx
features.tsx
pricing.tsx
cta.tsxRègle décisionnelle
| Situation | Où ? |
|---|---|
| 1 seule page | app/.../<page>/_components/ |
| 2+ pages distinctes | components/<thématique>/ |
| Primitive UI réutilisable | components/ui/ |
| Layout global | components/layout/ |
Pas de "components/forms/", pas de "components/admin/" — si un composant n'est utilisé que dans une page, il reste à côté.
Groupes de routes
app/
├── [locale]/
│ ├── (public)/ # Header + Footer publics
│ ├── (auth)/ # Layout centré (login/register/...)
│ ├── (onboarding)/ # Setup post-signup
│ ├── (account)/ # Espace user — UserShell
│ ├── (dashboard)/ # Espace admin — AdminShell
│ ├── (profile)/ # u/[username] — profil public
│ └── docs/ # Fumadocs
├── api/
├── maintenance/ # Hors [locale] — mode maintenance
├── r/[code]/ # Hors [locale] — redirect parrainage
└── layout.tsx # Root + AnnouncementBanner globalLes pages toujours sous app/[locale]/, sauf : api/, maintenance/,
r/, u/, globals.css, not-found.tsx, robots.ts, sitemap.ts.
proxy.ts (ex middleware.ts)
Next.js 16 a renommé middleware.ts en proxy.ts. Combine :
- Auth.js (récup session JWT)
- next-intl (matching locale, redirect)
- Maintenance check (redirect vers
/maintenancesi non-admin) - 2FA gate (redirect vers
/two-factorsipending2fa) - Update
UserSession.lastActiveAt
Toujours utiliser getAppUrlFromRequest(req) pour construire des URLs
absolues — pas req.url ni req.nextUrl.origin (retournent localhost
en LAN).
lib/
lib/
├── auth.ts # Auth.js config + JWT callbacks + impersonation
├── auth-lockout.ts # Brute-force protection
├── two-factor.ts # Code 2FA email
├── prisma.ts # Singleton Prisma
├── email.ts # Senders + sendTemplated
├── email-templates.ts # DEFAULT_TEMPLATES + types
├── plans.ts # CRUD + cache plans
├── plans-display.ts # Helpers rendu
├── coupons.ts # validateCoupon + redeemCoupon
├── site-config.ts # SiteConfig + pickLocalized
├── audit.ts # logAudit
├── notifications.ts # In-app notifications
├── geoip.ts # IP → country/city
├── sessions.ts # UserSession helpers
├── referral.ts # Codes + cookie ref
├── username.ts # Génération + validation
├── url.ts # getAppUrl, getAppUrlFromRequest
├── api-key-auth.ts # Auth via header Bearer
├── dashboard-stats.ts # Stats live (cache 60s)
├── money.ts # formatMoney(cents, currency, locale)
├── copy.ts # Clipboard cross-context (LAN sans HTTPS)
├── onesignal.ts # Push notifications (optionnel)
└── utils.ts # cn() (clsx + tailwind-merge)Providers (root layout)
app/layout.tsx empile :
<NextIntlProvider>
<ThemeProvider>
<SessionProvider>
<AnnouncementBanner />
<ImpersonationBanner />
{children}
<Toaster />
</SessionProvider>
</ThemeProvider>
</NextIntlProvider>Conventions
- Server Components par défaut,
"use client"seulement si nécessaire - Zod systématique sur API routes et forms
- Pas de string en dur — tout passe par
useTranslations/getTranslations(voir i18n) - Pas de fallbacks défensifs ni de validation pour cas qui ne peuvent pas arriver — le typage Prisma + Zod garantit l'invariant
- API routes :
auth()en première ligne,rolecheck pour admin - 100% TypeScript, zéro
.js
Allez plus loin
- DataList — composant
components/ui/ - i18n — conventions hooks
- Déploiement — variables d'env