Passe QA backoffice - ce qui reste
Plan de la refonte "experience editeur" du modele de contenu. A lire au debut de toute session qui touche au modele, aux blocs ou aux seeders. L'origine : le modele etait en texte la ou il devait etre en entites, et la bibliotheque de blocs etait illisible pour un editeur.
Etat au 2026-07-30, branche develop, dernier commit dcfbcd6.
La regle qui prime sur tout : zero regression de style
Confirme par l'utilisateur. Ce n'est pas un objectif, c'est une contrainte dure : toute la barre de qualite du projet est un diff mesure 1:1 contre le live, donc tout changement visuel non voulu est un defaut.
Deux portes, et le choix depend de la nature du changement :
| Nature du changement | Porte | Attendu |
|---|---|---|
| Non visuel (modele, libelles, nettoyage) | baseline.mjs --against current | zero pixel |
Visuel par construction (supprimer une bande codee en dur et la re-composer en blocs, aplatir de l'Elementor, remplacer productFacts) | diff.mjs contre le LIVE | pas pire qu'avant, puis re-baseline |
cd tools/fidelity
# iteration rapide (~3 min) pendant le developpement
MSYS_NO_PATHCONV=1 node baseline.mjs --label wip --against current --site pdn --widths 1440
# porte complete AVANT chaque commit (~40 min, 54 pages x 5 largeurs)
MSYS_NO_PATHCONV=1 node baseline.mjs --label check --against current⚠️ La porte ne prouve QUE "je n'ai rien change". Elle est aveugle aux ecarts qui existaient deja contre le live - la grille des activites de l'accueil gilliard et les gabarits d'article PDN (mesures a 29,6 % du live) en sont exactement. Ne jamais rapporter "porte verte" comme "conforme au live".
Preferer les changements qui ne rendent RIEN tant qu'aucun contenu n'est saisi : la signature d'auteur, les grilles contentBlocks et les champs logo/favicon retombent tous sur la sortie actuelle a l'octet pres quand ils sont vides. C'est ce qui permet d'ouvrir des capacites sans toucher au 1:1.
Deja fait, et qui debloque la suite
La Bibliothèque existe desormais sur les trois marques du perimetre (SeedSession.SeedLibrary, appele par les trois seeders) : gilliard (Vins, Activités Oenotouristiques), PDN (Cocktail, News, Event), chevaliers (coquille seule). Les noeuds sont publies - un noeud non publie s'affiche normalement dans le picker cote backoffice mais ne resout rien au rendu, piege silencieux. Ils n'ont pas de template, donc pas d'URL : absents des sitemaps, /bibliotheque/ repond 404.
C'est la condition prealable des pickers : avant, le picker Auteur ouvrait un arbre vide partout. SeedLibrary retourne nom -> Guid, c'est ce qu'il faut pour cabler category au chantier 1.
Ce qui reste, dans l'ordre
1. ✅ Pickers de categorie - FAIT (2026-07-30)
articleFields.category -> Core - Picker - Categorie (max 1, donc ModelsBuilder genere un IPublishedContent simple) et childListing.filterCategory -> Core - Picker - Categories (illimite -> IEnumerable<IPublishedContent>). Tout a change d'un bloc, comme exige.
childListing.cshtml passe par un helper Cat() et apparie par cle de noeud. Le helper Slug() reste applique au NOM pour l'URL ?category= : les adresses n'ont pas bouge (verifie, ?category=vins et ?category=activites-oenotouristiques rendent les memes articles qu'avant).
⚠️ Le piege a connaitre : un MultiNodeTreePicker stocke une LISTE d'udi separee par des virgules, meme plafonne a un element. ContentPick (un udi nu, pour Umbraco.ContentPicker) n'est donc PAS reutilisable : ecrite dans une propriete MNTP, sa valeur se relit comme vide, sans erreur a l'enregistrement ni au rendu - la categorie disparait, point. D'ou NodePick(params Guid[]) dans DevSeedSupport.cs.
Cote seeders : SeedLibrary remonte AVANT les articles (il faut ses cles), et les deux seeders echouent fort sur une categorie absente de la Bibliotheque plutot que de laisser un champ vide. heroPretitle garde le NOM de la categorie - c'est du texte d'affichage, pas une reference, et le live imprime bien la categorie au-dessus du H1.
Verifie : barre de filtre et pastilles resolues sur /blog/, et l'agregat PDN tient l'arithmetique (news 25 + evenements 5 = news-events 30). Porte complete a zero pixel sur 54 pages x 5 largeurs.
location reste volontairement un TextBox : un picker de venue serait vide sur 3 marques sur 4 (seul Gilliard a des venue), donc une regression par rapport au champ libre.
2. factsPanel (fin de la phase 3) - VISUEL PAR CONSTRUCTION
factItem + le DataType Core - Facts existent deja. Reste :
- composition
factsPanelportantfacts, appliquee aproduct,vacancy,activity,event; - supprimer les 6 proprietes d'affichage de
productFactset les 6 libelles codes en dur deViews/Partials/layout/_pageFacts.cshtml:10-15; - rendre la liste ; garder les champs types qui portent de la LOGIQUE (
startDate/endDatepour le tri,applyEmailpour le mailto,closingDate,location).
3. Repeteur de champs de formulaire (fin de la phase 3)
formField + Core - Form Fields sur formBlock, avec optionsSource (MNTP) pointant sur les vrais noeuds venue : les 5 noms de salles de formBlock.cshtml:118-124 (Salle des Foudres, Salle des Chais, Clos de la Cochetta, Clos du Mont, Carnotzet) dupliquent du contenu qui existe deja. Rendre fields s'il est rempli, sinon retomber sur les presets actuels pour ne rien casser, puis supprimer les presets une fois les seeders a jour. Ajouter aussi le JSON-LD FAQPage pour faqList.
4. Fin de la phase 4 - VISUEL PAR CONSTRUCTION
- Supprimer les deux bandes codees en dur de
BlogPost.cshtmlet les re-composer en blocs dans lecontentBlocksdesormais disponible ::59-60cherchent les pages par segment d'URL en dur ("vins"/"activites") - renommer l'URL de la page Vins tue la bande en silence ;:81-82« Partager notre passion » / « Nos Activités » ;:105« Retrouvez d'autres articles ». - Textes de chrome vers le Dictionnaire (le handler uSync est en
CreateOnly, donc sans risque) : « Lire plus » / « Découvrir » (_listCard.cshtml:41,51), « Toutes les catégories » (childListing.cshtml:120), « Dates : » / « Lieu : » (Event.cshtml:52,56), « Contrat » / « Taux » / « Département » / « Lieu » / « Postuler » (Vacancy.cshtml:11-14,35), « Sur les réseaux » (_footer.cshtml:93- le seul intitule de colonne sans propriete derriere), le texte de consentement de la carte (map.cshtml:28-29). Event.cshtml:18codefr-FRen dur pour le formatage des dates, quelle que soit la culture.Event.cshtml:31supprime la liste des faits si le corps riche contient un<strong>: un editeur qui met un mot en gras fait disparaitre les dates et le lieu.- Liens morts livres en production :
_header.cshtml:111,118(compte/panier enhref="#"avec un compteur0fige) et les formulaires newsletter enaction="#"(_footer.cshtml:48,newsletterSignup.cshtml:20). Cabler, masquer derriere un interrupteursiteSettings, ou retirer. Vacancy.cshtml:closingDateest saisissable et n'est rendu nulle part.
5. Fin de la phase 5
- Importer les 21 images encore hotlinkees sur
gilliard.chdans les corps d'articles : la production sert aujourd'hui ces images depuis le WordPress live du client. ReutiliserDevSeedSupport.ImportMediaAsync, puis reecrire lessrc. - Elaguer les 15 exports WXR en double dans
tools/.
6. Phase 6
Parcours backoffice complet - la checklist est dans ../modules/backoffice.md, section « Revue "experience editeur" ». Aucune commande ne la remplace.
Parite entre marques - a verifier a CHAQUE commit
Le modele est majoritairement partage, donc la plupart des changements touchent les trois marques sans rien faire : articleFields (compose par blogPost et pdnArticle), siteSettings, pageSettings, les 4 grilles de blocs, les partials communs, et SeedHtml qui vit dans Rich().
Mais les doctypes propres a une marque derivent en silence - c'est exactement comme ca que chevaliersVacancy s'est retrouve sans grille de blocs alors que vacancy en avait recu une. Verifications rapides :
cd src/Web/uSync/v17/ContentTypes
grep -l "<Alias>contentBlocks</Alias>" *.config # qui a une grille
grep -l "<composition></Composition>" *.config # qui compose quoiGilliarday est hors perimetre (decision utilisateur) mais herite des changements partages sans dommage - ne pas le retirer des grilles ni des compositions.
Defauts de contenu CLIENT - a signaler, PAS a corriger
Verifie directement contre le live chevaliers /fr/contact/ : mailto:maison@chevaliers.ch est libelle maison@gilliard.ch, mailto:gruppen@chevaliers.ch est libelle reception@gilliard.ch, tel: 455 28 28 est libelle 329 89 29, et les src des icones sont en http://.
Le live porte lui-meme tous ces defauts. Notre seed les reproduit fidelement, ce qui est correct pour une migration 1:1 - les corriger serait une deviation deliberee et releve d'une decision du client.
Le harnais - etat connu
tools/fidelity/baseline.mjs est la porte locale (local vs local). Cinq causes de non-determinisme ont ete trouvees et corrigees :
- instantane unique de
document.images(b-lazy fait entrer des images en chargement APRES) ; - attendre "tout charge" alors qu'une
<img>masquee n'est jamais telechargee par Chrome, donc restecomplete=falsea vie et brulait tout le delai (17,9 s au lieu de 1,7 s) ; decode()attendu nu ne se resout jamais pour une image qui n'a jamais commence a charger - il figeait la capture sans sortie ni erreur (c'est l'origine des processus zombies) ;- elements a apparition non reveles (
IntersectionObserverasynchrone) ; - images de fond CSS : elles ne sont pas dans
document.images, donc aucune des attentes ci-dessus ne les voyait.
Aucune cause connue ne reste ouverte. Deux executions consecutives du meme build sont identiques.
Le signe distinctif du bruit : un nombre de pixels identique qui revient d'une execution a l'autre (302, 327, 3072) veut dire qu'UN element bascule, pas que la page est bruyante. Une vraie regression ne reproduit pas un compte identique depuis une autre cause.

