Case study UI/UX · Ouidou · septembre 2026
Un CMS où l'IA écrit les pages, et l'humain garde la main
FCD Gen est un mini CMS local, sans installation : une colonne pour le site, un assistant à côté, et chaque page produite en un fichier HTML complet, SEO et GEO ready. Ce document raconte comment l'interface a été pensée pour qu'un modèle de langage serve un site cohérent plutôt que vingt-six pages qui se ressemblent vaguement.
L'enjeu
Générer une page, c'est facile. Générer un site, non.
Un modèle de langage sait écrire une belle page d'accueil. Demandez-lui la vingt-sixième, et l'en-tête a changé, le menu a oublié deux rubriques, et personne ne sait si la page tient sur un écran de portable.
Le besoin de départ était simple : produire le site institutionnel de la FCD — pages, blog, salle de presse — en s'appuyant sur l'IA, sans retomber dans le copier-coller d'un chat vers un éditeur de code.
Le vrai sujet de design est ailleurs. Une IA générative est très bonne pour proposer et très mauvaise pour se souvenir. L'interface doit donc porter tout ce que le modèle ne peut pas garantir seul : la cohérence d'une page à l'autre, la preuve que la qualité est là, et la certitude qu'aucune modification ne s'applique dans le dos de la personne qui édite.
D'où une règle tenue sur tout le projet : ce qui doit être identique partout est écrit par l'application, pas par le modèle.
- L'habillage dériveUn gabarit unique : le modèle ne rend que le
<main>, l'application assemble en-tête, pied et métadonnées. - Le menu se périmeLa navigation est régénérée à chaque assemblage, depuis les pages réelles.
- La qualité est invisibleUn audit de 53 contrôles, avec des mesures dans le navigateur plutôt qu'une lecture du CSS.
- Une retouche casse le resteRégénération ciblée : les sections non cochées ne traversent jamais l'API.
- L'IA applique sans demanderToute proposition du modèle est vérifiée, prévisualisée, puis appliquée ou ignorée.
Démarche
Le prototype d'abord, la maquette ensuite
Inverser l'ordre habituel était un choix : sur un outil piloté par l'IA, on ne sait pas ce qui pose problème avant de l'avoir vu tourner sur un vrai site.
Prototype fonctionnel
Sidenav, chat, aperçu, code, audit, visuels. Branché à Gemini, testé sur le site FCD réel.
Revue UX mesurée
Heuristiques de Nielsen et WCAG 2.2 AA, chaque constat mesuré dans le navigateur ou lu dans le code.
Design system Figma
246 composants, propriété
Fidélité = Haute | Wireframe, 688 variantes.Figma remis au réel
Le dashboard dessiné documente ce qui est construit, plus ce qui avait été proposé.
Restitution
Ce case study, et ses spécifications front pour la suite du développement.
Architecture de l'écran
Trois colonnes, aucune barre horizontale
Tout ce qui se règle vit dans la colonne de gauche — y compris les outils de l'éditeur. La barre du haut a été supprimée : elle recouvrait le coin de la page en aperçu.
Une colonne, deux états
La navigation du site — Contenu, Salle de presse, Bibliothèques, Site — ou, page ouverte, les outils d'édition de cette page. Un retour la ramène.
Un assistant par page
Chaque contenu a sa propre conversation et son historique de versions. La colonne se règle à la poignée et se replie entièrement.
Une scène, trois vues
Tableau de bord par défaut, liste des contenus par rubrique, ou éditeur avec Aperçu, Code, Audit et Visuels.
Replier pour se concentrer
Les deux colonnes latérales se replient : la navigation en rail d'icônes, l'assistant complètement, avec une languette pour le rappeler. L'animation passe par grid-template-columns, donc rien ne saute.
Le jeu de 33 icônes a été redessiné au trait le 17/09 : la première version, isométrique, ne se lisait plus à 20 px. Une icône qui doit être reconnue en rail ne peut pas être une illustration.
Le choix est mémorisé, comme la largeur de l'assistant et la dernière vue ouverte : on revient là où on était.
Tableau de bord modulable
Une grille fermée : l'écran ne défile pas, il se remplit
Chaque widget est la version compacte de quelque chose que l'outil sait déjà faire. On les déplace, les redimensionne, les répartit sur jusqu'à huit écrans.
pack, si bien qu'aucun modèle ne peut contenir de chevauchement.Les gestes
| Geste | Effet |
|---|---|
| Glisser une carte par son en-tête | La déplacer sur une place libre |
| La lâcher sur une autre carte | Les deux permutent |
| Tirer le coin bas droit | Redimensionner, sans recouvrir une voisine |
| Croix sur l'en-tête | Retirer de l'écran |
| « + » dans les onglets | Un écran de plus, renommable au double-clic |
Ajouter à un écran plein est refusé
La hauteur des lignes se calcule sur la place réellement disponible : tout tient d'un coup d'œil. La contrepartie est dite à l'utilisateur, avec un message qui propose d'ouvrir un onglet.
Une carte lâchée sans la place revient à son point de départ, plutôt que d'en pousser trois autres en cascade. La prévisibilité passe avant la densité.
Du filaire au réel
Le wireframe Figma du 24/09 a été recalé sur le prototype, pas l'inverse : même grille, mêmes dimensions mesurées dans le code. Faites glisser pour comparer.
Créer un contenu
La question « quel genre de page ? » se pose au bon moment
Le choix du gabarit se fait dès la création, filtré par le type de contenu — pas après coup dans un dialogue séparé.
Page
WebPageNaît au menu. 12 gabarits : landing, service local, étude de cas, tarifs, centre d'aide…
Article
BlogPostingDaté, hors menu, vit dans l'index du blog.
Communiqué
NewsArticle8 gabarits aux conventions du genre : dateline, pyramide inversée, citation attribuée, faits citables.
Dossier de presse
Report6 gabarits, ressources téléchargeables et kit presse ZIP généré.
Le type détermine trois choses : le brief envoyé au modèle, les données structurées — écrites par l'application, pas par le modèle — et l'index dans lequel le contenu apparaît.
data-site-nav déclenche la synchronisation du menu sur toutes les pages.L'éditeur
Voir, retoucher, comparer — sans format intermédiaire
L'édition porte sur le DOM réellement rendu. Ce qu'on voit, ce que le modèle retravaille et ce qui est exporté sont le même document.
allow-scripts). Sortir du mode crée une version « édition manuelle », donc réversible.Régénérer une partie seulement
Refaire une page entière pour corriger une section en abîme d'autres : le modèle reformule toujours un peu ce qu'il réécrit. Régénérer des sections liste les blocs de premier niveau avec leur titre et leur poids, et laisse cocher ceux à refaire.
Seules les sections cochées sont envoyées. Les autres ressortent identiques, au caractère près, parce qu'elles ne quittent jamais le navigateur. Si la réponse ne respecte pas le format, rien n'est modifié — et l'outil le dit.
Une autre version, pas une retouche
Autre version demande une alternative : même sujet, mêmes informations, même identité, mais une autre composition. La structure actuelle est transmise avec la consigne de s'en écarter, et la température est majorée le temps de cette requête.
La variante s'ajoute comme une version étiquetée : les deux compositions restent comparables et interchangeables par le sélecteur de versions.
Qualité mesurée
Mesurer plutôt que déclarer
L'audit ne lit pas le CSS pour deviner : il rend la page dans des iframes isolés et mesure les contrastes nœud par nœud, le fond réellement affiché, les trous de mise en page et le premier écran.
- 96SEO
- 78GEO / IA
- 100Accessibilité (statique)
- 95Performance
- 96Bonnes pratiques
Deux contrôles géométriques
Occupation de l'espace parcourt chaque grille et chaque flex à 1280 px et signale toute rangée qui laisse plus d'un quart de la largeur vide — deux cartes dans une grille à trois colonnes, une dernière rangée orpheline.
Ligne de flottaison rend la page sur trois formats. Une taille de titre calculée sur la seule largeur donne 144 px sur un écran 1920×900 et coupe tout le premier écran : l'échelle du kit se dimensionne donc sur min(vw, svh).
Le score d'accessibilité est statique : il ne remplace ni le clavier, ni le lecteur d'écran. La revue UX recommande de le renommer.
La revue UX du 07/09 : l'outil était moins bon que ses pages
Résultat le plus important de la revue, et le plus rassurant : le kit UI tient le niveau AA sur les pages produites. Les défauts de l'interface, eux, sont nombreux mais très localisés — un seul jeton de couleur explique les trois quarts des écarts.
| Constat | Mesure | Correctif proposé | État au 02/10 |
|---|---|---|---|
| Textes secondaires sous AA | 31 nœuds · 2,68:1 | --ink-3 de #9E9E9E à #6E6E6E | Ouvert |
| Bordures des champs | 1,14:1 (3:1 requis) | Jeton --field-line:#8A8A8A | Ouvert |
| Onglets de l'éditeur muets | role="tab" seul | aria-selected, aria-controls, tabpanel | Ouvert |
| Messages système non annoncés | #toast sans rôle | role="status" aria-live="polite" | Ouvert |
| Onglet Audit trop long | 91 % de « conforme » | Replier les contrôles conformes | À planifier |
| Textes trop petits | 43 nœuds < 12 px | Plancher de 12 px, cibles de 24 px | À planifier |
| Contraste des pages produites | 0 écart sur l'accueil | — | Conforme |
Le lot 1 (contrastes, bordures, toast, onglets) est estimé à une heure et mesurable juste après. Il n'est pas encore appliqué : c'est la première chose à faire avant une démonstration client.
Thème
Un sombre qui n'est pas un clair inversé
Deux réglages distincts, clairs par défaut : l'outil, et les pages qu'il produit. En sombre, les surfaces s'éclaircissent pour s'élever, puisqu'une ombre n'y est plus visible.
Côté pages produites, l'instruction système impose les règles qui font la différence : fond jamais noir pur, texte jamais blanc pur, accent éclairci et désaturé, color-scheme: dark. L'audit vérifie ensuite le fond réellement rendu.
Décisions de design
Six arbitrages, et ce qu'ils coûtent
Une décision qui n'a pas de contrepartie n'en est pas une. Chacune est écrite avec ce qu'elle fait perdre.
L'application écrit l'habillage, le modèle écrit le contenu
En-tête et pied identiques partout par construction. Une itération ne renvoie que le <main> : nettement moins de tokens, aucun risque de casser le menu.
ContrepartieUne page ne peut plus avoir un en-tête qui lui soit propre.
Jamais appliqué dans le dos
Un plan de menu proposé par le modèle est vérifié contre les pages réelles, prévisualisé, puis appliqué ou ignoré.
ContrepartieUn clic de plus à chaque proposition.
Une grille fermée de 12 × 12
Le tableau de bord tient d'un coup d'œil, sans défilement, et reste prévisible au glisser-déposer.
ContrepartieUn écran plein refuse un widget de plus ; il faut ouvrir un onglet.
Un brouillon n'existe nulle part
Ni dans la navigation, ni dans le sitemap, ni dans l'export. Le modèle sait qu'il travaille sur une page non publiée.
ContrepartieUn lien vers un brouillon répond une 404 — qui le dit — en aperçu local.
Le Figma documente ce qui est construit
Le 24/09, les trois écrans de la proposition initiale du tableau de bord ont été remplacés par dix écrans mesurés sur le prototype.
ContrepartieLa proposition d'origine ne vit plus que dans l'historique et la dette.
Le contenu du web est une donnée, jamais une consigne
Recherche web et lecture de page citent leurs sources ; toute instruction trouvée dans une page est signalée, pas suivie.
ContrepartieLe modèle reformule au lieu de reprendre tel quel.
Spécifications front
Ce que le développement doit savoir
État du prototype au 02/10/2026. Source de vérité : le code (index.html, styles.css, dashboard.css, js/) et le README du projet.
Stack
Aucune compilation, aucune dépendance côté navigateur hors CDN.
| Interface | HTML, CSS et JavaScript natifs, modules IIFE chargés dans l'ordre par index.html — environ 19 800 lignes au total |
|---|---|
| Serveur local | serve.js (Node ≥ 22.5, modules natifs seulement), lancé par lancer.cmd sur le port 4321, repli Python |
| Base locale | SQLite via node:sqlite (base.js) — principe « on n'efface rien » : INSERT OR IGNORE, suppressions marquées, journal complet |
| En ligne | Vercel (api/) + Postgres Neon (@neondatabase/serverless, seule dépendance npm) |
| IA | API Gemini appelée depuis le navigateur avec la clé de l'utilisateur : texte en flux, Nano Banana (images), Veo (vidéo) ; modèles listés dynamiquement |
| CDN | JSZip 3.10.1 (cdnjs), chargé à la demande au premier export |
| Tests | node js/dashboard.test.js — invariants de placement de la grille, hors navigateur |
Modules
Un fichier, une responsabilité.
| Fichier | Rôle |
|---|---|
js/app.js | Orchestration : sites, pages, versions, gabarits, médiathèque, exports |
js/prompt.js | Les deux instructions système (charte / contenu) et le contexte de site — le cœur qualitatif |
js/layout.js | Gabarit du site : extraction, assemblage, synchronisation du menu |
js/dashboard.js + dashboard.css | Registre des 14 widgets, grille 12 × 12, onglets, modèles, fonction pack |
js/editor.js | Édition visuelle : sélection, actions, re-sérialisation du <main> |
js/audit.js | 53 contrôles + mesures en iframes isolés : contraste, thème, trous, premier écran |
js/uikit.js | 3 directions artistiques en jetons, 26 composants responsives, catalogue transmis au modèle |
js/arbo.js | Arborescence en éditeur de nœuds |
js/gemini.js · js/assets.js | Client API ; remplissage des visuels, cache IndexedDB, réencodage WebP |
js/store.js · js/db.js | Persistance locale et réconciliation avec la base du serveur |
js/templates.js · js/thumbs.js | 27 gabarits (briefs structurés) ; miniatures SVG assemblées depuis 30 blocs filaires |
Jetons de l'interface
Palette interfaces Ouidou : un bleu unique #2962FF, jamais de marine en accent. Le marine sert la colonne de navigation.
| Typographie | --font : Aptos, Calibri, Segoe UI, system-ui · --mono : Cascadia Mono, Consolas · corps 14 px |
|---|---|
| Rayon | --radius:14px — au-dessus de la référence de 12 px pour les cartes ; à ramener à 12 px avec les contrôles à 8 px |
| Élévation | --shadow (posé), --shadow-lg (flottant), --shadow-bloc pour les widgets |
| Thème | Clair par défaut ; uiTheme = light | dark | auto, appliqué avant le premier rendu |
Layout et points de rupture
Une grille CSS à trois colonnes, animée.
/* styles.css */ :root{ --navw:221px } body{ --chat-w:30% } .layout{ display:grid; grid-template-columns: var(--navw) clamp(280px, var(--chat-w), 62vw) 1fr; transition: grid-template-columns .3s cubic-bezier(.4,0,.2,1); } body.nav-collapsed{ --navw:56px } /* rail d'icônes */ body.chat-collapsed .layout{ grid-template-columns: var(--navw) 0 1fr }
| Rupture | Effet |
|---|---|
1240px | Navigation resserrée à 196 px |
900px · 860px | Dialogues en une colonne ; widgets du tableau de bord réorganisés |
760px | La grille du tableau de bord s'empile en une colonne, le glisser-déposer s'éteint |
prefers-reduced-motion | Six blocs de règles : transitions et animations neutralisées |
Grille du tableau de bord
Géométrie mesurée sur le code, reportée telle quelle dans le Figma.
| Cases | 12 colonnes × 12 lignes, gouttière 14 px ; hauteur de ligne calculée sur la place disponible |
|---|---|
| À 1204 px utiles | colonne 87,5 px · ligne 54,5 px · panel 6×5 = 595 × 328,5 · chiffre 3×2 = 290,5 × 123 |
| Widgets | 14, en 5 familles : chiffres (4), travail (4), contenu (3), site (2), actions (1) |
| Écrans | 8 onglets au maximum, renommables ; 5 modèles |
| Persistance | localStorage fcdgen.dashboard.v1, par site — hors export et hors sauvegarde JSON, ce n'est pas du contenu |
Contrat HTML des pages produites
Le modèle pose des emplacements ; l'application les remplit. C'est ce qui rend les pages vivantes sans régénération.
<!-- navigation synchronisée sur toutes les pages --> <nav data-site-nav>…</nav> <!-- index reconstruit à chaque assemblage : une seule carte modèle --> <div data-index="communique" data-limit="5" data-category="Sécurité alimentaire"> <article> <img data-field="image"> <h3 data-field="title"></h3> <time data-field="date"></time> <a data-field="link">Lire</a> </article> </div> <!-- visuels : jamais d'URL écrite par le modèle --> <img src="" alt="…" width="1600" height="900" data-ai-image="prompt" data-ar="16:9"> <img src="" data-stock="keywords"> <video data-ai-video="prompt"></video> <img data-asset="logo-fcd"> <!-- fichier joint au rôle « Intégrer » --> <ul data-downloads>…</ul> <!-- ressources presse, une ligne modèle -->
Un emplacement non rempli affiche un visuel d'attente SVG aux bonnes dimensions, jamais une image cassée. L'export ajoute sitemap.xml, robots.txt, et pour un site daté feed.xml et llms.txt.
Données et persistance
Le navigateur est la copie de travail, la base garde l'historique.
localStorage | fcdgen.sites.v1 (sites, pages, versions) · fcdgen.settings.v1 (réglages, clé API) · fcdgen.dashboard.v1 |
|---|---|
| IndexedDB | Visuels générés (indexés par prompt, réencodés en WebP) et pièces jointes binaires |
| Routes serveur | GET/PUT /api/db/etat · /api/db/page · /api/db/version · /api/db/visuel(s) · /api/fetch (lecteur de page) · /api/preview |
| Réconciliation | La base n'est adoptée que si le navigateur est vide ou plus ancien qu'elle ; jamais de remplacement silencieux d'un contenu local |
| Sécurité | Clé API en clair dans localStorage, envoyée directement à Google : à proscrire sur un poste partagé |
Accessibilité
Cible WCAG 2.2 AA, comme tous les livrables Ouidou.
| En place | Icônes SVG aria-hidden avec libellé visible, aria-expanded sur les sections, journal du chat en aria-live="polite", prefers-reduced-motion respecté |
|---|---|
| À corriger | Contraste de --ink-3 et des bordures de champs ; onglets de l'éditeur sans aria-selected ni tabpanel ; toast non annoncé ; libellés de groupe de la colonne à mesurer sur #12293F |
| Non dessiné | États de focus clavier dans le Figma ; alternative clavier au glisser-déposer du tableau de bord |
Performance
Pour l'outil, et pour les pages qu'il produit.
| Visuels | PNG du modèle réencodé en WebP s'il allège réellement — mesuré sur 18 visuels : 15,3 Mo → 2,7 Mo |
|---|---|
| Tokens | Seul le contenu de la page part au modèle ; seule la dernière version est transmise en entier |
| Pages produites | Médias à dimensions réservées, slider en scroll-snap sans JS, cible 100/100 Lighthouse — à confirmer dans PageSpeed sur l'URL définitive, l'audit ne mesure ni LCP, ni INP |
| Vidéo | Veo n'est déclenchée que si la page contient un data-ai-video : longue et coûteuse |
Dette et suite
Ce qui reste à trancher
Sans ce relevé, la personne qui reprend la maquette croit que tout est décidé. Extrait du journal de dette du tableau de bord.
Le modèle Atelier perd la moitié de ses widgets
Actions rapides occupe 12 × 10 cases : trois widgets sont écartés sans message. Le test le signale déjà. À trancher : 12 × 7, ou deux onglets.
Les widgets n'ont pas d'état « édition »
En Personnaliser, l'app ajoute poignée, croix et coin ; le Figma ne les montre pas. Propriété Édition = Non | Oui à créer.
Onze entrées dans la section Site
La colonne défile à 1040 px de haut. Replier Site par défaut, ou sortir les quatre entrées de fichier dans un sous-menu.
Chargement, erreur, focus, refus de dépôt
Listés plutôt que dessinés : génération en cours, clé refusée, quota dépassé, écran plein. Le focus clavier d'abord.