FCD Gen — case study

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.

Rôle
UX/UI, prototype, design system Figma — Jean-Marie Bruce
Période
07/09 → 01/10/2026, en continu sur le prototype
Cas réel
Site institutionnel de la FCD : 26 contenus, 11 entrées de menu
Moteur
API Gemini (texte, Nano Banana, Veo), clé de l'utilisateur
26contenus du site FCD servis par un seul gabarit
27gabarits filtrés par type de contenu, chacun avec sa miniature
53contrôles d'audit, dont contrastes et premier écran mesurés
14widgets sur une grille fermée de 12 × 12 cases
246composants Figma pilotés par une propriété de fidélité
0étape de compilation : on ouvre, ça tourne

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.

  1. Prototype fonctionnel

    Sidenav, chat, aperçu, code, audit, visuels. Branché à Gemini, testé sur le site FCD réel.

  2. Revue UX mesurée

    Heuristiques de Nielsen et WCAG 2.2 AA, chaque constat mesuré dans le navigateur ou lu dans le code.

  3. Design system Figma

    246 composants, propriété Fidélité = Haute | Wireframe, 688 variantes.

  4. Figma remis au réel

    Le dashboard dessiné documente ce qui est construit, plus ce qui avait été proposé.

  5. 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.

Personnaliser. Glisser par l'en-tête, lâcher sur une autre carte pour les permuter, tirer le coin pour redimensionner.
Bibliothèque. Cinq familles — chiffres, travail, contenu, site, actions. La coche dit ce qui est déjà à l'écran.
Modèles. Un modèle ne décrit que des noms de widgets ; les positions sont calculées à l'ouverture par la fonction pack, si bien qu'aucun modèle ne peut contenir de chevauchement.

Les gestes

GesteEffet
Glisser une carte par son en-têteLa déplacer sur une place libre
La lâcher sur une autre carteLes deux permutent
Tirer le coin bas droitRedimensionner, sans recouvrir une voisine
Croix sur l'en-têteRetirer de l'écran
« + » dans les ongletsUn écran de plus, renommable au double-clic
Choix assumé

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.

Wireframe Figma du tableau de bord, en niveaux de gris. Le même tableau de bord dans le prototype, en haute fidélité.

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é.

Cinq étapes. Type, sujet, intention, direction, récapitulatif. Le brief qui en sort reste relisible avant l'envoi au modèle.
Des miniatures qui dessinent la structure. Un brief n'a pas de page à capturer : la vignette assemble en SVG les blocs qu'il demande — 30 blocs filaires.

Page

WebPage

Naît au menu. 12 gabarits : landing, service local, étude de cas, tarifs, centre d'aide…

Article

BlogPosting

Daté, hors menu, vit dans l'index du blog.

Communiqué

NewsArticle

8 gabarits aux conventions du genre : dateline, pyramide inversée, citation attribuée, faits citables.

Dossier de presse

Report

6 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.

Arborescence. Un fil d'une prise à une carte rattache une page ; un double-clic l'ouvre.
Navigations. La proposition du modèle est vérifiée contre les pages réelles : un identifiant inventé est écarté, la raison affichée.
Un seul en-tête. Le conteneur marqué 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.

La colonne de gauche est passée en outils d'édition : Vue (Aperçu, Code, Audit, Visuels), puis Reprendre la page.

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.

ConstatMesureCorrectif proposéÉtat au 02/10
Textes secondaires sous AA31 nœuds · 2,68:1--ink-3 de #9E9E9E à #6E6E6EOuvert
Bordures des champs1,14:1 (3:1 requis)Jeton --field-line:#8A8A8AOuvert
Onglets de l'éditeur muetsrole="tab" seularia-selected, aria-controls, tabpanelOuvert
Messages système non annoncés#toast sans rôlerole="status" aria-live="polite"Ouvert
Onglet Audit trop long91 % de « conforme »Replier les contrôles conformesÀ planifier
Textes trop petits43 nœuds < 12 pxPlancher de 12 px, cibles de 24 pxÀ planifier
Contraste des pages produites0 é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.

InterfaceHTML, CSS et JavaScript natifs, modules IIFE chargés dans l'ordre par index.html — environ 19 800 lignes au total
Serveur localserve.js (Node ≥ 22.5, modules natifs seulement), lancé par lancer.cmd sur le port 4321, repli Python
Base localeSQLite via node:sqlite (base.js) — principe « on n'efface rien » : INSERT OR IGNORE, suppressions marquées, journal complet
En ligneVercel (api/) + Postgres Neon (@neondatabase/serverless, seule dépendance npm)
IAAPI 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
CDNJSZip 3.10.1 (cdnjs), chargé à la demande au premier export
Testsnode js/dashboard.test.js — invariants de placement de la grille, hors navigateur

Modules

Un fichier, une responsabilité.

FichierRôle
js/app.jsOrchestration : sites, pages, versions, gabarits, médiathèque, exports
js/prompt.jsLes deux instructions système (charte / contenu) et le contexte de site — le cœur qualitatif
js/layout.jsGabarit du site : extraction, assemblage, synchronisation du menu
js/dashboard.js + dashboard.cssRegistre 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.js53 contrôles + mesures en iframes isolés : contraste, thème, trous, premier écran
js/uikit.js3 directions artistiques en jetons, 26 composants responsives, catalogue transmis au modèle
js/arbo.jsArborescence en éditeur de nœuds
js/gemini.js · js/assets.jsClient API ; remplissage des visuels, cache IndexedDB, réencodage WebP
js/store.js · js/db.jsPersistance locale et réconciliation avec la base du serveur
js/templates.js · js/thumbs.js27 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.

--blue#2962FF · #5B8BFF
--blue-bg#EBF0FF · #1B2740
--nav-bg#12293F
--page#F5F7FF · #0F1218
--surface#FFFFFF · #171B23
--ink#1F1F1F · #E8EAF0
--ink-3#9E9E9E · à corriger
--ok-fg · --warn-fg#2C7A31 · #C97A1E
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èmeClair 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 }
RuptureEffet
1240pxNavigation resserrée à 196 px
900px · 860pxDialogues en une colonne ; widgets du tableau de bord réorganisés
760pxLa grille du tableau de bord s'empile en une colonne, le glisser-déposer s'éteint
prefers-reduced-motionSix 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.

Cases12 colonnes × 12 lignes, gouttière 14 px ; hauteur de ligne calculée sur la place disponible
À 1204 px utilescolonne 87,5 px · ligne 54,5 px · panel 6×5 = 595 × 328,5 · chiffre 3×2 = 290,5 × 123
Widgets14, en 5 familles : chiffres (4), travail (4), contenu (3), site (2), actions (1)
Écrans8 onglets au maximum, renommables ; 5 modèles
PersistancelocalStorage 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.

localStoragefcdgen.sites.v1 (sites, pages, versions) · fcdgen.settings.v1 (réglages, clé API) · fcdgen.dashboard.v1
IndexedDBVisuels générés (indexés par prompt, réencodés en WebP) et pièces jointes binaires
Routes serveurGET/PUT /api/db/etat · /api/db/page · /api/db/version · /api/db/visuel(s) · /api/fetch (lecteur de page) · /api/preview
RéconciliationLa 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 placeIcônes SVG aria-hidden avec libellé visible, aria-expanded sur les sections, journal du chat en aria-live="polite", prefers-reduced-motion respecté
À corrigerContraste 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.

VisuelsPNG du modèle réencodé en WebP s'il allège réellement — mesuré sur 18 visuels : 15,3 Mo → 2,7 Mo
TokensSeul le contenu de la page part au modèle ; seule la dernière version est transmise en entier
Pages produitesMé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éoVeo 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.

Défaut du prototype

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.

Design system

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.

Architecture

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.

États

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.