Architecture technique
Le Labo du Yeti est un CMS HTML headless mono-instance, conçu pour le modèle SaaS hybride 1 client = 1 instance Plesk. Cette page décrit la stack complète, les couches du système, les flux de requêtes et les choix d'architecture qui ont structuré le projet. Tous les composants ci-dessous sont en production et fonctionnent ensemble dans un seul processus Node.js par instance client.
Philosophie d'architecture
Le projet répond à quatre contraintes structurantes :
- Mono-instance par client — chaque domaine tourne son propre processus Node, sa propre DB, son propre
JWT_SECRET. Pas de mutualisation, donc pas de fuite cross-tenants possible. - Plesk-compatible — la stack ne dépend que de Node + npm. Pas de Docker, pas de Redis, pas de Postgres. La DB est un fichier
content/cms.dbembarqué. - Sortie 100 % HTML statique — le public final n'exécute aucun JavaScript serveur. Le rendu écrit des fichiers dans
public/, qui sert deDocumentRootPlesk. - Extensible — un système d'extensions WordPress-like permet d'ajouter routes, hooks, sidebar et pages custom sans toucher au coeur.
Stack backend
Runtime & framework HTTP
| Composant | Version | Rôle |
|---|---|---|
| Node.js | 22 LTS | Runtime ESM ("type": "module"). Choisi pour les prebuilds better-sqlite3 et le support natif fetch. |
| Express | 4.x | Framework HTTP. Routeur principal monté sous /api, gestion des middlewares en chaîne. |
| helmet | recent | En-têtes de sécurité (CSP, X-Frame-Options, etc.). |
| express-rate-limit | recent | Limiteurs spécialisés : loginLimiter, aiGenLimiter, aiCallLimiter, submitLimiter, restoreLimiter. |
| cors | recent | Origine contrôlée par CORS_ORIGIN (env Plesk). |
Persistance & sécurité
| Composant | Rôle |
|---|---|
better-sqlite3 | Driver SQLite synchrone. PRAGMA journal_mode=WAL, synchronous=NORMAL, foreign_keys=ON, busy_timeout=5000. |
bcryptjs | Hash des mots de passe utilisateurs et fallback pur-JS sans dépendance native. |
jsonwebtoken | JWT signés (JWT_SECRET par instance). Source d'authentification primaire. |
multer | Upload multipart. Profils dédiés : uploadHtml, uploadImage, uploadMedia, plus un upload ZIP 10 Mo pour les extensions et 200 Mo pour les restaurations. |
nodemailer 8 | SMTP universel (login 2FA, reset password, soumissions de formulaires, rappels cocon). |
adm-zip | Création des archives backup/export et inspection des ZIP d'extensions. |
sharp | Traitement d'images (crop favicon en WebP 512×512, conversion par l'extension webp-auto-converter). |
Pipeline HTML
Le coeur du rendu public repose sur Cheerio, un parser HTML jQuery-like côté serveur. Tous les templates uploadés (pages, articles, cocon, layout global) sont lus en mémoire puis transformés en cascade :
- Injection du contenu CMS sur les attributs
data-cmsetdata-cms-field. - Remplissage des slots IA sur les attributs
data-aipour les articles de blog et pages cocon. - Composition layout + page : le layout global fournit le
head, le header et le footer ; la page fournit le contenu inséré dans<main data-cms-slot="content">. - SEO automatique :
title,meta description, Open Graph, JSON-LD, canonical, hreflang. - Réécriture des URLs media en chemins relatifs depuis
public/.
Le résultat est écrit en HTML statique dans public/ par le service buildSite(), qui produit aussi sitemap.xml, robots.txt et la page 404 personnalisable. Voir Moteur HTML pour le détail du pipeline.
Stack frontend
Le frontend est une SPA React buildée par Vite, servie depuis frontend/dist/. En production, le backend Express n'expose pas le panel directement : il est monté sous un slug admin aléatoire pour rendre la surface d'attaque moins visible.
| Composant | Version | Rôle |
|---|---|---|
| React | 18 | UI principale, hooks-first, pas de class components. |
| Vite | 5 | Bundler dev + prod. Build produit frontend/dist/ commité sur la branche release. |
| react-router-dom | 6 | Routage SPA. Routes protégées par un guard lisant auth-store. |
| zustand | recent | Stores d'état globaux légers : auth-store (user, token, capabilities), theme-store (light/dark, accent). |
| Tailwind CSS | 3 | Utility-first sur des design tokens CSS centralisés dans frontend/src/styles/tokens.css. |
| axios | recent | Client HTTP avec intercepteurs JWT et gestion globale des 401/403. |
Design tokens
Toutes les couleurs, espacements et radius passent par des variables CSS définies dans tokens.css. Tailwind est configuré pour les consommer via theme.extend. Cela permet :
- Un switch light/dark instantané via
data-themesur<html>. - Un système d'accent dynamique (orange
#F58220par défaut, navy#0F1B3Den secondaire). - Un gradient unifié sidebar/login :
linear-gradient(180deg, #0A1547 0%, #131F5E 30%, #2A2B97 70%, #4A3FB8 100%).
Flux d'une requête HTTP
Une requête authentifiée vers l'API suit toujours la même chaîne de middlewares. Exemple type pour POST /api/blog/articles :
Client (axios)
│
▼
helmet ← en-têtes de sécurité (CSP, HSTS, XFO)
│
▼
cors ← validation Origin contre CORS_ORIGIN
│
▼
express.json() / multer ← parsing body ou multipart
│
▼
rateLimit (loginLimiter, ← protection brute-force / spam IA
aiGenLimiter, ...)
│
▼
/api router ← dispatch vers le sous-router (auth, blog, …)
│
▼
requireAuth ← extrait JWT ou token API, charge req.user
│
▼
requireRole('owner','admin', ← vérifie l'appartenance au set de rôles
'webmaster',
'editor')
│
▼
requireCapability('blog') ← matrice rôle × feature (SQLite)
│
▼
controller (blog-controller) ← validation payload, appel service
│
▼
service (blog-service) ← logique métier, orchestrations IA
│
▼
db-store (better-sqlite3) ← INSERT/UPDATE transactionnel
│
▼
Réponse JSON ← status + body sérialisé
Chaque couche a une responsabilité unique :
| Couche | Responsabilité | Ne fait jamais |
|---|---|---|
helmet / cors | Sécurité réseau, en-têtes HTTP. | Logique métier. |
rateLimit | Quotas par IP sur les endpoints sensibles. | Authentification. |
requireAuth | Décodage JWT ou validation token API, peuplement req.user. | Décisions d'accès fines. |
requireRole | Vérification dure du rôle utilisateur. | Vérifications par feature. |
requireCapability | Vérification dynamique de la matrice persistée. | Bypass owner (owner court-circuite cette étape). |
| controller | Validation du payload HTTP, traduction REST → service. | Accès direct à la DB. |
| service | Règles métier, orchestrations multi-tables, appels IA. | Manipulation du req/res. |
db-store | Helpers SQL préparés, transactions. | Logique métier. |
requireCapability permet de désactiver une feature pour un rôle sans toucher au code (matrice éditable via /api/capabilities).Flux du rendu d'une page publique
Quand un build est déclenché par POST /api/build, le service build-service orchestre le rendu de toutes les pages, articles et pages cocon. Pour chaque entité, le pipeline est :
- Lecture du layout global — le HTML actif est lu depuis
uploads/layouts/<active>.html. - Lecture du template de la page — le HTML uploadé via
POST /api/pagesou généré par l'IA pour les articles/cocon. - Appel
render-servicequi :- Parse layout et page avec
cheerio.load(). - Injecte les
data-cms(texte, href, src, alt, classes) depuispage.overrideset les helpers globaux (header/footer). - Injecte les
data-ai(titres, paragraphes, FAQ, JSON-LD) depuis le contenu généré. - Compose : insère la page dans le slot
<main data-cms-slot="content">du layout. - Génère le bloc SEO (title, meta, Open Graph, canonical, hreflang, JSON-LD).
- Réécrit les URLs des médias vers
./assets/media/relatifs à la position du fichier final.
- Parse layout et page avec
- Hook
render.html— toute extension peut transformer le HTML final (ex. surefeedback-connector injecte un snippet avant</body>). - Écriture —
fs.writeFileSync()verspublic/<slug>/index.html(oupublic/<parent>/<enfant>/index.htmlpour les cocons enfants). - Génération sitemap — collecte de toutes les URLs publiées, écriture de
public/sitemap.xmletpublic/robots.txt.
Voir Moteur HTML (pipeline) pour le détail du Cheerio, et Build & Publication pour l'orchestration.
Schedulers internes
Deux schedulers tournent dans le processus Node principal. Ils s'appuient sur setInterval avec une garde au boot pour éviter les doubles exécutions en cas de redémarrage rapide.
Notifications cocon
Toutes les 5 minutes, le scheduler scanne les cocon_groups dont le quota anti-spam vient de se débloquer. Si un créneau est libre depuis moins de 5 minutes, un email est envoyé via SMTP aux utilisateurs ayant les rôles webmaster ou chef_projet (SLOT_NOTIFICATION_ROLES). Voir Cocon SEO.
Publication blog programmée
Le scheduler blog scanne les articles avec status='scheduled' et scheduled_at <= NOW(). Il bascule leur statut en published et déclenche un buildSite() ciblé sur l'article et l'index blog. Cela permet la publication différée sans intervention manuelle. Voir Blog IA.
Synthèse des couches
| Couche | Fichiers types | Dépendances autorisées |
|---|---|---|
| HTTP / middleware | backend/src/middleware/*.js | express, helmet, jwt, db-store (lecture only) |
| Routes | backend/src/routes/*.js | controllers, middleware |
| Controllers | backend/src/controllers/*.js | services, validation |
| Services métier | backend/src/services/*.js | db-store, autres services, cheerio |
| Persistance | backend/src/utils/db-store.js | better-sqlite3 uniquement |
| Pipeline rendu | backend/src/services/render-service.js | cheerio, fs, db-store |
| Extensions | extensions/installed/<id>/index.js | contexte ext.* injecté (sandbox) |
Cette discipline de couches est ce qui rend le code maintenable malgré l'absence de framework lourd type Nest ou Adonis. Chaque ajout de feature passe par les mêmes étapes : route → controller → service → db-store, plus éventuellement un hook d'extension. Voir Structure des dossiers pour la cartographie complète.
Ce que l'architecture évite volontairement
- Pas d'ORM lourd. Les requêtes SQL préparées dans
db-store.jssont explicites et auditées. Pas de N+1 invisibles. - Pas de queue externe. Les jobs lourds (génération cocon batch, build) tournent dans le process avec un suivi de progression in-memory exposé via
GET /api/cocon/groups/:id/generation-progress. - Pas de SSR React. Le rendu public est pur HTML statique. Le React n'existe que dans le panel admin.
- Pas de microservices. Un seul process Node par client. Le scaling se fait par instance, pas par découpage horizontal.
Pour aller plus loin, lire ensuite Structure des dossiers qui détaille l'arborescence physique du repo, puis Base de données pour le schéma SQLite complet.