Modele de contenu
Tout est serialise via uSync sous src/Web/uSync/v17/. Les modeles C# correspondants sont generes par ModelsBuilder dans src/Web/umbraco/Models/*.generated.cs (ne pas editer). Le modele livre est volontairement minimal (gabarit) avec des exemples a adapter.
Document types (uSync/v17/ContentTypes/)
| Alias | Nom | Role |
|---|---|---|
homePage | Page d'accueil | Racine de site. Compose cookiesSettings, marketingSettings, pageSettings, siteSettings. Template homePage. |
pageSettings | Page Settings | Composition SEO/sitemap partagee par les pages (meta description, masquage sitemap...). |
notFoundPage | Page 404 | Page d'erreur 404 par site (resolue par NotFoundContentFinder). Compose pageSettings. |
internalServerErrorPage | Page 500 | Page d'erreur 500 (cible de ErrorController). Compose pageSettings. |
cookiesSettings | Cookies Settings | Composition : textes/labels du popup de consentement cookies. |
marketingSettings | Marketing Settings | Composition : ID Google Analytics 4. |
Les blocs d'exemple du template (
exampleSection,exampleElement,genericSection,titleBlock+ leurs DataTypesCustom - Bloc List - Example Element/ `Custom - Block Grids
- Base`) ont ete supprimes (nettoyage 2026-07-15) : ils encombraient l'editeur (onglets « Generic »/« Contenu » sur la home, bloc « Titre » bleu dans tous les champs RichText). Le jeu de blocs reel est la bibliotheque Gilliard (voir plus bas).
DataTypes (uSync/v17/DataTypes/)
Les 46 DataTypes sont ranges dans des conteneurs (dossiers) cote backoffice via l'element <Folder> de chaque .config (les fichiers restent a plat sur le disque ; Level="2"). uSync recree les conteneurs a l'import (au demarrage, groupe Settings). Dossiers : Dropdowns, DatePickers, Pickers, Media, Uploads, Labels, ListViews, Fields, Blocks. Pour deplacer un DataType, changer son <Folder> (ou le glisser dans le backoffice, ExportOnSave re-serialise).
Bibliotheque d'editeurs preconfigures, prete a l'emploi :
- Pickers media :
MediaPicker,ImageMediaPicker,MultipleMediaPicker,ImageCropper, variantes legacy. - Pickers contenu/URL :
ContentPicker,MemberPicker,SingleURLPicker,MultiURLPicker. - Champs :
Textstring,Textarea,RichtextEditor,Numeric,Decimal/DecimalPositiveOnly,Truefalse,Dropdown/DropdownMultiple,DropdownHeadingSizes,Radiobox,CheckboxList,Tags,DatePicker/DatePickerWithTime,ApprovedColor. - BlockGrid / BlockList :
CustomBlockGridsBase,CustomBlocListExampleElement. - Labels (lecture seule) :
LabelString,LabelInteger,LabelBigint,LabelDecimal,LabelDatetime,LabelTime,LabelBytes,LabelPixels. - ListViews :
ListViewContent,ListViewMedia,ListViewMembers. - Uploads media :
UploadFile,UploadArticle,UploadAudio,UploadVideo,UploadVectorGraphics. seo:Umbraco.TextBoxconfigure (maxChars 20).
Property editors custom (App_Plugins/)
Editeurs de propriete maison, reutilisables pour creer des DataTypes dans le backoffice (voir aussi modules/backoffice.md) :
- ButtonPicker (
App_Plugins/ButtonPicker/) - property editor UISpektrum.PropertyEditorUi.ButtonPicker(schemaUmbraco.TextBox) : un groupe de boutons single-select ou la valeur stockee est la classe CSS du bouton choisi (emise par les vues de blocs). La liste des boutons ({ label, value }), ladefaultValueet l'option "boutons compacts" se configurent sur le DataType via l'editeur de configSpektrum.PropertyEditorUi.ButtonPickerConfig(repeater libelle / classe). 100 % frontend (aucun backend C#). Aucun DataType prelivre : creer unData Typebase sur "Button Picker" et y definir les boutons.
MediaTypes / MemberTypes / Templates
- MediaTypes :
image,file,folder,umbracoMediaArticle,umbracoMediaAudio,umbracoMediaVideo,umbracoMediaVectorGraphics. - MemberTypes :
member(standard). - Templates :
homepage,contentpage,textpage,blogpost,event,vacancy,venue,notfoundpage,internalservererrorpage. Les 4 templates de liste (bloglist/eventlist/vacancylist/venuelist) ont ete supprimes au profit du blocchildListingsurContentPage.
Langues (uSync/v17/Languages/)
fr(French) : langue par defaut (IsDefault=true).en-US: marqueeChange="Delete"dans uSync (en cours de suppression) - ne pas considerer comme active.- Le backoffice est en
fr-FR(DefaultUILanguage,appsettings.json).
Dictionnaire (uSync/v17/Dictionary/)
Cles d'interface multilingues, surtout pour le consentement cookies (cookies.*, cookiespopup*), le footer (footer.*), l'accessibilite (accessibility.skiptocontent, accessibility.noscript...) et des cles generiques. Le handler Dictionnaire uSync est en CreateOnly (n'ecrase pas les traductions existantes a l'import).
Libellés dynamiques de blocs - UFM, plus AngularJS
Le champ Label d'un bloc (Block List et Block Grid) affiche un résumé de l'élément dans la liste du backoffice. Il peut afficher la valeur d'une propriété du bloc.
La syntaxe a changé avec le backoffice v14. Umbraco 13 et avant utilisaient AngularJS, donc {{alias}}. Depuis la v14, le backoffice est en Lit et le champ utilise UFM (Umbraco Flavored Markdown). {{alias}} n'est plus interprété : il s'affiche littéralement dans la liste des blocs.
Symptôme : les blocs de la liste s'intitulent tous {{title}} au lieu du titre saisi. Rien ne casse par ailleurs, ce qui rend le défaut facile à laisser passer.
La syntaxe UFM
Forme générale : {<prefixe>: <contenu>}.
| Usage | Syntaxe | Exemple |
|---|---|---|
| Valeur d'une propriété | {umbValue: alias} | {umbValue: headline} |
| Idem, forme courte a privilegier | {=alias} | {=title} |
| Clé de traduction | {umbLocalize: cle} ou {#cle} | {#general_name} |
| Nom du contenu pointé par un picker | {umbContentName: alias} | {umbContentName: page} |
| Titre du lien d'un picker de liens | {umbLink: alias} | {umbLink: ctaLink} |
| Expression JavaScript | ${ ... } | ${ title.toUpperCase() } |
Filtres
Ils s'enchaînent avec | :
{umbValue: bodyText | stripHtml | wordLimit:15}
{=title | fallback:"Sans titre"}
{=name | uppercase}Disponibles : bytes, fallback, lowercase, stripHtml, titleCase, truncate, uppercase, wordLimit.
Expressions JavaScript
${ ... } évalue une expression dans un bac à sable : accès aux propriétés, opérateurs logiques et conditionnels, arithmétique, méthodes natives.
${ title }
${ title.length > 0 ? title : "Sans titre" }
${ link[0].name }
${ navMenuTitle || (navMenuLink && navMenuLink[0] && navMenuLink[0].name) || '' }Le dernier exemple est le patron à retenir pour les pickers de liens (MultiUrlPicker) : la valeur est un tableau, il faut donc garder chaque niveau pour ne pas casser quand le champ est vide. Le || '' final évite d'afficher undefined.
Pour ce cas précis, {umbLink: navMenuLink} fait la même chose de façon déclarative. Préférer le composant quand il existe, et réserver ${ ... } aux cas qu'il ne couvre pas (repli sur une autre propriété, condition, mise en forme).
Choisir la forme
{=alias}par défaut. C'est court et ça couvre la majorité des blocs.{=alias | fallback:"..."}dès qu'un bloc peut exister sans que la propriété soit remplie.{umbLink:}/{umbContentName:}pour les pickers.${ ... }seulement pour la logique que les trois précédents ne savent pas exprimer.
Le defaut a bien ete livre ici, malgre cette page. Au 2026-07-29, six Block List portaient encore
{{alias}}:CoreHeroSlides({{title}}),CoreMainMenuetCoreMenuLinks({{label}}),CorePeople({{name}}),CoreSocialLinks({{network}}),CoreTeamGroups({{heading}}). SeulCoreCardsetait correct. C'est exactement le symptome decrit plus haut - invisible tant qu'on n'ouvre pas le backoffice. Tous corriges avec unfallback:(ce queCoreCardsfaisait deja et que les six n'auraient pas eu autrement) : lancer la commande ci-dessous fait partie de la revue, la lire ne suffit pas.
Détecter l'ancienne syntaxe
À lancer après toute reprise d'un projet v13, ou après un copier-coller depuis un ancien dépôt. Retour vide = conforme.
grep -rn '{{' src/Web/uSync/*/DataTypes/ || echo "aucun label AngularJS"Référence
Documentation officielle : docs.umbraco.com, section Model your content -> Property editors -> Umbraco Flavored Markdown. Le champ Label du Block List y renvoie explicitement.
Icônes de blocs et de document types - toujours vérifier qu'elles existent
Chaque document type porte une icône (<Icon> dans son .config uSync). Pour un element type exposé dans une Block Grid ou une Block List, c'est l'icône que l'éditeur voit dans le sélecteur de blocs.
Une icône inventée ne casse rien de visible côté dev : la solution compile, l'import uSync passe, le site rend normalement. Le seul symptôme est une case vide dans le sélecteur de blocs du backoffice. Le défaut n'apparaît donc qu'en ouvrant l'éditeur, souvent après la livraison.
Règle : ne jamais écrire un nom d'icône de mémoire. Toujours le vérifier contre le registre Umbraco avant de créer ou de modifier un bloc.
Format
<Icon>icon-home color-black</Icon>Le premier segment est le nom de l'icône, le second (optionnel) sa couleur (color-black, color-red, color-amber, ...). Seul le premier segment doit exister dans le registre.
Où est le registre
Le backoffice v14+ n'utilise plus un fichier SVG par icône. Les noms vivent dans un manifeste JavaScript livré par le paquet Umbraco.Cms.StaticAssets :
~/.nuget/packages/umbraco.cms.staticassets/<version>/staticwebassets/umbraco/backoffice/packages/core/icons-<hash>.jsLe hash change à chaque version d'Umbraco, donc on résout le fichier au lieu de le coder en dur. Umbraco 17.6.0 expose 713 icônes.
Vérifier une icône
# Toutes les icones disponibles, triees
M=$(find ~/.nuget/packages/umbraco.cms.staticassets/17.6.0 -name 'icons-*.js' ! -name '*.map')
LC_ALL=C grep -o 'name: "icon-[a-z0-9-]*"' "$M" | sed 's/name: "//; s/"//' | sort -u
# Chercher par mot-cle
LC_ALL=C grep -o 'name: "icon-[a-z0-9-]*"' "$M" | sed 's/name: "//; s/"//' | grep alignValider tout le dépôt d'un coup
À lancer après avoir ajouté ou modifié des blocs. Retour vide = conforme.
M=$(find ~/.nuget/packages/umbraco.cms.staticassets/17.6.0 -name 'icons-*.js' ! -name '*.map')
LC_ALL=C grep -o 'name: "icon-[a-z0-9-]*"' "$M" | sed 's/name: "//; s/"//' | sort -u > /tmp/icons.txt
grep -rho "<Icon>[^< ]*" src/Web/uSync/ | sed 's/<Icon>//' | sort -u | while read -r i; do
grep -qx "$i" /tmp/icons.txt || echo "INCONNUE $i"
doneDeux icones inventees etaient en production au 2026-07-29 :
richIntroportaiticon-align-left- le cas decrit juste en dessous, jamais corrige - etactivityportaiticon-compass, qui n'existe pas non plus (la famille s'appelleicon-directions). Corrigees enicon-text-align-lefteticon-directions. La validation complete ci-dessus passe desormais a vide ; c'est elle qui les a trouvees, pas une relecture.
Le cas qui a motivé la règle
Un bloc richIntro a été livré dans un projet aval avec <Icon>icon-align-left</Icon>. Cette icône n'existe pas : le registre v17 nomme la famille icon-text-align-left / -center / -right / -justify. Le nom paraissait évident, il ne l'était pas. Résultat : bloc sans icône dans le sélecteur, découvert bien après coup.
Icônes usuelles pour des blocs
Vérifiées présentes en 17.6.0, à utiliser comme point de départ plutôt que d'inventer :
| Usage | Icône |
|---|---|
| Bloc générique, section | icon-block, icon-item-arrangement |
| Texte, intro, citation | icon-font, icon-article, icon-blockquote |
| Alignement de texte | icon-text-align-left, icon-text-align-center |
| Image, galerie | icon-picture, icon-pictures-alt-2, icon-thumbnail-list |
| Vidéo, audio | icon-video, icon-sound-waves |
| Carrousel, diaporama | icon-slideshow, icon-billboard |
| Grille, liste | icon-grid, icon-list, icon-split |
| Personnes, équipe | icon-user, icon-users, icon-users-alt |
| Lien, bouton, partage | icon-link, icon-share |
| Formulaire, contact | icon-mailbox, icon-message |
| Carte, localisation | icon-map-location |
| Agenda, date | icon-calendar |
| Code, embed | icon-code |
| Navigation, réglages | icon-navigation, icon-settings |
Après une montée de version d'Umbraco
Le registre change entre majeures : des icônes disparaissent, d'autres sont renommées. Rejouer la validation complète ci-dessus fait partie de la procédure de montée de version (voir ../architecture/build-deploy.md).
Blocs Gilliard (page d'accueil) + mapping d'import
Bibliotheque de blocs reutilisables pour la reproduction de gilliard.ch (Block Grid GilliardContentBlocks sur homePage). Ensemble volontairement deduplique : un seul cardCollection couvre les rangees de cartes (hero-boxes, "Nos Activites", provisoirement "Nos Gammes"). Vues : Views/Partials/blocks/<alias>.cshtml (+ delegation blockgrid/Components/<alias>.cshtml) ; styles styles/base/sections/_<nom>.scss ; JS scripts/modules/{heroSlider,cardCarousel}.js.
Le tableau sert aussi de carte de mapping pour l'import ulterieur du contenu WordPress (champ source -> propriete Umbraco). Proprietes plates, contenu separe du style (ButtonPicker), listes repetees en Block List imbriquee (1 item source -> 1 bloc).
| Bloc (alias, IsElement) | Proprietes (alias -> editeur) | Source WP (home) |
|---|---|---|
heroSlider | slides -> BlockList de heroSlide | N2 Smart Slider (hero) |
heroSlide | image -> MediaPicker (photo de fond) ; pretitle,title,highlight -> TextBox ; ctaLink -> MultiURLPicker ; layout -> Dropdown Core - Hero Layout (photo|banner, invariant) ; bannerImage -> MediaPicker ; bannerLink -> MultiURLPicker | slides du hero. highlight = pastille promo (ex. « 20% »), vide hors promo. bannerImage/bannerLink = visuel produit de la colonne gauche (layout banner uniquement). layout=banner = photo de fond + visuel promo a gauche + colonne texte centree a droite ; layout=photo (defaut si vide) = photo plein cadre, texte centre |
cardCollection | pretitle,title -> TextBox ; display -> ButtonPicker Core - Card Display (invariant) ; cards -> BlockList de card | hero-boxes / Nos Activites / Nos Gammes / cartes contact |
card | image -> MediaPicker ; icon -> MediaPicker ; title -> TextBox ; text -> Textarea ; richText -> RichText ; link -> MultiURLPicker ; price -> TextBox | carte individuelle. icon renseigne -> mode « info » centré (cartes contact) ; sinon carte image avec price ou text -> mode « activité » (g-card--activity : pastille or, titre or majuscules, ligne d'adresse + pin) — grilles Activités/Oenotourisme ; richText prioritaire sur text |
mediaText | image -> MediaPicker ; secondImage -> MediaPicker (affichage duo uniquement) ; icon -> MediaPicker ; display -> ButtonPicker Core - Media Text Display (invariant) ; imageOnRight -> Truefalse ; pretitle,title -> TextBox ; body -> RichText ; ctaLink -> MultiURLPicker | rangees .team-info-row (about, valeurs, vinotheque, ambassadeurs, cochetta). Le traitement est display, jamais deduit (voir ci-dessous). Bouton dans le corps = btn btn-primary (contour or) |
richIntro | display -> ButtonPicker Core - Rich Intro Display (section|prose, invariant) ; pretitle,title -> TextBox ; body -> RichText | section (defaut, et le comportement historique) = intro de section .section-title-underline (titre or souligne, centre, colonne de prose a 7/12). prose = page de texte suivi (mentions legales, conditions) : titre a gauche sans souligne, corps sur toute la largeur du conteneur, rythme de paragraphe serre. Voir ci-dessous |
ctaBanner | backgroundImage -> MediaPicker ; video -> MediaPicker (invariant) ; videoControls -> Truefalse (invariant) ; pretitle,title -> TextBox ; text -> RichText ; cta -> MultiURLPicker | bande CTA pleine largeur .hero-custom-banner.left. video renseignee -> variante cta-banner--video (le « video hero » de gilliarday) : la video remplace le fond, backgroundImage reste le poster. Sans videoControls la video est decorative (muette, en boucle, aria-hidden) |
formBlock | heading -> TextBox ; intro -> RichText ; formType -> Dropdown(CoreFormType: contact/inquiry/booking/newsletter) ; consentLabel,submitLabel -> TextBox ; narrow -> Truefalse | .section-form + CF7 (contact / adhésion / réservation) |
map | query -> TextBox (adresse ou lat,lng) ; zoom -> TextBox | carte Google .acf-map (400px pleine largeur) |
newsletterSignup | title -> TextBox ; text -> Textarea ; backgroundImage -> MediaPicker3 (optionnel) | bandeau newsletter ; backgroundImage renseigne -> variante newsletter-band--image (photo cave sombre + scrim, comme le live) ; vide -> bande dorée |
childListing | heading -> TextBox ; source -> ContentPicker (vide = page courante) ; layout -> ButtonPicker Core - Listing Layout (cards|cards-blog|cards-archive|cards-teaser|links|rows) ; order -> ButtonPicker Core - Listing Order (manual|agenda|date-desc) ; filterCategory -> TextBox (liste separee par virgules : News,Event agrege plusieurs categories, cf. PDN /news-events/) ; showCategoryFilter -> Truefalse ; max -> Numeric ; emptyText -> TextBox ; excerptWords -> Numeric ; ctaLink -> MultiURLPicker | liste les enfants d'une page. Remplace les 4 templates de liste (BlogList, EventList, VenueList, VacancyList, supprimes) : ils ne differaient que par le tri et le traitement des cartes. source fait d'une archive une vue et non du contenu duplique (c'est ce qui permettra a PDN d'afficher cocktails/news/evenements comme 3 vues d'un seul jeu de posts). order=agenda = a venir du plus proche au plus lointain, puis passes du plus recent au plus ancien. Trois cartes distinctes, jamais des variantes l'une de l'autre (mesurees sur le live) : cards-blog = la grille 3-up du blog Gilliard (pastille de categorie SUR l'image, ni date ni categorie dans le corps, « Lire plus » en toutes lettres) ; cards-archive = l'archive de PDN, UNE carte par rangee (image 505x448 calee a gauche, titre Yeseva One, ligne date + categorie sous le titre, extrait, fleche seule) ; cards-teaser = la carte teaser de l'accueil PDN (image carree + categorie au-dessus du titre + lien icone). Les confondre a deja coute deux regressions : la carte d'archive posee sur l'accueil a fait des bandes de 3781px contre 818, et sa ligne meta posee sur cards-blog a imprime « 1 janvier 0001 » a cote de chaque titre de Gilliard. ctaLink rend un bouton centre sous la liste (« Voir tous nos cocktails » — le live ferme sa bande teaser ainsi) |
embed | provider -> Dropdown Core - Embed Provider (weezevent|weezpay|html) ; embedId -> TextBox ; html -> Textarea (si provider=html) ; consentCategory -> Dropdown Core - Consent Category (vide = functional) ; height -> Numeric ; title -> TextBox | widget tiers, consentement implemente une seule fois ici. La charge utile est rendue inerte dans un <template> : avant consentement le navigateur ne fait aucune requete vers le tiers. consentEmbed.js la clone dans le DOM apres acceptation (et recree les <script>, qu'un clone de <template> n'execute pas). Sert Weezevent + WeezPay (gilliarday). Chevaliers n'en utilise aucun : le widget Airbnb du plan n'existe pas, et le moteur Seekda du live est masque (display:none, 0 px) donc non porte — cf. domain/assets-substitues.md §6 |
mediaGallery | pretitle -> TextBox (sur-titre, live « Suivez-nous sur Instagram ») ; title -> TextBox ; linkUrl -> TextBox (optionnel) ; display -> ButtonPicker Core - Media Gallery Display (invariant) ; images -> MultipleMediaPicker | feed "@maisongilliard" (display=square-fullbleed). Ex-instagramFeed : un mur Instagram n'est qu'un affichage parmi d'autres, donc le meme bloc sert aussi les editions precedentes et le collage de gilliarday. Sans linkUrl les tuiles ne sont pas cliquables |
Traitement visuel richIntro : display decide, et il n'est jamais deduit du contenu. section est le defaut et ne change rien a l'existant. prose existe parce qu'une page de mentions legales n'est pas une suite d'intros de section : elle n'a pas de bande de titre, son texte est aligne a gauche et il remplit le conteneur. Rendu .rich-intro--prose ; l'habillage mesure est dans themes/pdn/_overrides.scss (registre §C19), pas dans base/, donc les autres marques ne le voient pas tant qu'elles ne l'ont pas mesure de leur cote.
Cote seeder PDN le choix est derive du scrape, pas d'une liste de slugs : une bande dont le widget de titre est un h3 est de la prose (isProseBand() dans pdn-gen.mjs). Sur les 14 pages Elementor de PDN une seule remplit ce critere, et c'est aussi la seule dont le h1 est vide - d'ou l'absence de banniere sur cette page.
Traitement visuel mediaText : colonne texte centree (padding 50px), emblème icon au-dessus du titre, titre 16px / w400 / majuscules / or, corps 16px gris centre (max 570px). imageOnRight inverse l'ordre. pretitle/ctaLink optionnels.
⚠️ display a TROIS valeurs — le traitement est AUTORISE, jamais deduit. Le live nomme lui-meme ses variantes (verifie dans son style.min.css + son markup, mesure @1440, identique sur gilliard et chevaliers) :
display | live | rendu |
|---|---|---|
photo (defaut) | col-xl-6 row-img, pages interieures | photo 675x450 fixe (3:2), calee en haut (ne s'etire pas), emblème 40px |
feature | bande welcome-boxes-2 = la home | .row-img img{object-fit:cover;min-height:580px} + .logo-big img{max-width:160px} -> photo 675x580 pleine hauteur, grand emblème |
product | col-lg-2 row-img, /gammes/ uniquement | bouteille portrait 209x296 en contain |
Avant 2026-07-17 la variante etait deduite par icon == null -> --product, ce qui rendait les photos des deux home en « bouteille contenue » (bug reel, ~300px de hauteur perdus par site). Ne jamais reintroduire une deduction : display est un ButtonPicker, comme cardCollection.display.
cardCollection.text + cardCollection.cta : le paragraphe centre + le bouton que le live place sous la grille (son p.text + .btn-wrap a.btn, bloc de 163px sur l'accueil Chevaliers, « Découvrez nos gammes »). Rendus par .card-collection__foot. Optionnels : seule la bande des gammes s'en sert. Sans ces deux champs le bloc etait purement et simplement perdu.
Pages interieures — composition pageHero + doctype contentPage :
pageHero(composition,Folder=Compositions, Keyf1c33003-…-0001) :heroImage(MediaPicker3),heroPretitle,heroTitle(TextBox). Rend la banniere de titre.hero-page-banner(image plein cadre + voile.25+ H1 blanc 48px/12px centre) viaViews/Partials/layout/_pageHero.cshtml, appelee au-dessus de la grille par le template.contentPage(doctype,Folder=Pages, Keyf1b22001-…-0001, templateContentPage) : composepageHero+pageSettings(SEO) ; proprietecontentBlocks= grilleGilliardContentBlocks. Autorise en enfant dehomePage(+ auto-imbrication). Un seul doctype libre couvre les ~11 pages "block canvas" (about, valeurs, en-primeur, vendanges, activites x2, gammes, ambassadeurs, vinotheque, contact, newsletter).
Multi-marques — le patron <site>Canvas (Phase 0.4)
Une instance Umbraco, 4 marques. Chaque marque a son propre jeu de blocs dans le backoffice, mais le rendu reste partage : les element types ne sont jamais forkes.
Le mecanisme, en 3 pieces :
- Un BlockGrid DataType par marque —
Gilliard - Content Blocks,Chevaliers - Content Blocks,PDN - Content Blocks,Gilliarday - Content Blocks(dossierBlocks/<Marque>). Chacun ne liste que les element types de sa marque. C'est le seul endroit qui cadre ce que l'editeur voit. - Une composition
<site>Canvas(chevaliersCanvas,pdnCanvas,gilliardayCanvas,Folder=Compositions) dont l'unique propriete estcontentBlocks, liee a la grille de sa marque. - Des doctypes coquilles vides :
chevaliersHome=cookiesSettings+marketingSettings+pageSettings+siteSettings+chevaliersCanvas;chevaliersContentPage=pageHero+pageSettings+chevaliersCanvas. Zero<GenericProperty>, zero Razor.
| Marque | Home | Page de contenu | Canvas | Grille |
|---|---|---|---|---|
| Gilliard | homePage (…94dca878) | contentPage (…0001) | (inline, historique) | Gilliard - Content Blocks |
| Chevaliers | chevaliersHome (…0010) | chevaliersContentPage (…0013) | chevaliersCanvas (f1c33003-…0003) | Chevaliers - Content Blocks (f1d22002-…0012) |
| Porte de Novembre | pdnHome (…0011) | pdnContentPage (…0014) | pdnCanvas (…0004) | PDN - Content Blocks (…0013) |
| Gilliarday | gilliardayHome (…0012) | gilliardayContentPage (…0015) | gilliardayCanvas (…0005) | Gilliarday - Content Blocks (…0014) |
Doctypes structures de Chevaliers
| Doctype | Key | Compositions | Template | Parent autorise |
|---|---|---|---|---|
chevaliersActivity | …0016 | pageHero + pageSettings + chevaliersCanvas (+ price/location en ligne) | ContentPage | chevaliersActivityList |
chevaliersActivityList | …0017 | pageHero + pageSettings + chevaliersCanvas | ContentPage | racine, chevaliersContentPage |
chevaliersEvent | …0018 | pageHero + pageSettings + scheduleFacts + chevaliersCanvas | ContentPage | chevaliersEventList |
chevaliersEventList | …0019 | pageHero + pageSettings + chevaliersCanvas | ContentPage | racine, chevaliersContentPage |
chevaliersVacancy | …0020 | pageHero + pageSettings + vacancyFacts — coquille pure, zero propriete | Vacancy (partage, deja agnostique de marque) | chevaliersContentPage |
chevaliersVacancy : pourquoi il existe alors que le live n'a aucune offre. La .section-vacancies de /fr/domaines-chevaliers/ est un titre + prose editoriale permanente (le live l'affiche qu'il y ait des postes ou non) suivie d'une .positions-box VIDE — la boucle WordPress a zero ligne. On rend donc les deux : un richIntro (la prose) + un childListing layout=links, qui ne rend rien tant qu'aucun enfant n'existe (exactement la .positions-box vide) et qui liste les offres des qu'on en ajoute. Ajouter une offre = travail d'editeur, sans dev : creer une « Offre d'emploi — Domaines Chevaliers » sous la page Domaines Chevaliers, publier. Pas de chevaliersVacancyList ni de page /emploi/ : le plan en prevoyait, le live n'en a pas — la liste vit dans la page. Cf. domain/chevaliers-block-map.md.
teamGrid (…0012) fait partie du jeu Chevaliers - Content Blocks : /fr/domaines-chevaliers/ porte une bande equipe de 5 person-box (photo + nom + fonction) qui tombe 1:1 sur teamGrid > teamGroup > person (un seul groupe, sans titre de groupe).
Pourquoi ca marche sans nouveau Razor : Umbraco autorise N doctypes -> 1 template, et ModelsBuilder emet des interfaces de composition. HomePage.cshtml/ContentPage.cshtml sont untyped (Model.Value<BlockGridModel>("contentBlocks")), _header/_footer sont typees ISiteSettings, _pageHero IPageHero. Verifie apres regen : ChevaliersHome : …, ICookiesSettings, IMarketingSettings, IPageSettings, ISiteSettings et ChevaliersContentPage : …, IPageHero, IPageSettings.
⚠️ Gate invisible au dotnet build : tant que ChevaliersHome : ISiteSettings n'est pas genere, la fabrique de modeles renvoie un IPublishedContent nu, AncestorOrSelf<ISiteSettings>() renvoie null et l'en-tete/le pied/GA disparaissent silencieusement. Toujours verifier la ligne public partial class <X>Home : … apres un regen, pas seulement que ca compile.
Asymetrie assumee : Gilliard garde homePage + son contentBlocks en ligne (pas de gilliardCanvas). Le noeud 1105 est le seul site qui tourne ; chaque etape reste additive. Le cout est cosmetique (un <Name> explicite), le gain est de ne rien risquer sur le site vivant.
Ajouter un bloc a une marque = ajouter sa cle dans le blocks[] de SA grille. Rien d'autre : le renderer Partials/blocks/<alias>.cshtml existe deja et sert les 4 marques.
Ne PAS forker blogList/event/eventList/vacancyList/venue/venueList : specifiques a Gilliard. Les autres marques couvrent leurs listes avec contentPage + le bloc childListing.
Controle d'age (age gate)
La barriere de consentement (overlay .loading-page.age-restricted, rendue par Views/Partials/layout/_ageGate.cshtml depuis _MasterLayout pour toutes les marques) tire son texte de 5 champs sur la composition siteSettings (onglet Marque), donc chaque marque edite sa propre copie dans le backoffice :
ageGateHeading(TextBox),ageGateQuestion(TextArea),ageGateYesLabel/ageGateNoLabel(TextBox),ageGateDisclaimer(RTE, Gilliard uniquement ; vide -> bloc masque).
Le partial est type (Model.AgeGateHeading, ...) : ces champs ont ete ajoutes a sitesettings.config puis les modeles ont ete regeneres (voir le gate ModelsBuilder ci-dessus). Des defauts par marque gardent le gate fonctionnel avant toute configuration. Le style vit dans base/sections/_age-gate.scss (tokens par marque : --bs-primary pour l'accent, --agegate-logo-w, tracking du titre) ; le comportement (cookie age-verified, fondu, #age-yes qui ferme) dans scripts/modules/ageGate.js. Ajouter le gate a une marque = ajouter @use ".../age-gate" dans son main.scss (fait pour gilliard et chevaliers ; pdn/gilliarday restent a faire dans leur chantier).
Domaines Chevaliers — doctypes (Phase 1.3)
| Doctype | Cle | Compositions | Proprietes propres |
|---|---|---|---|
chevaliersHome | f1b22001-…0010 | cookiesSettings + marketingSettings + pageSettings + siteSettings + chevaliersCanvas | 0 |
chevaliersContentPage | …0013 | pageHero + pageSettings + chevaliersCanvas | 0 |
chevaliersActivity | …0016 | pageHero + pageSettings + chevaliersCanvas | 2 : price, location |
chevaliersActivityList | …0017 | pageHero + pageSettings + chevaliersCanvas | 0 |
chevaliersEvent | …0018 | pageHero + pageSettings + chevaliersCanvas + scheduleFacts | 0 |
chevaliersEventList | …0019 | pageHero + pageSettings + chevaliersCanvas | 0 |
chevaliersEvent est le retour sur investissement de la 0.5 : zero propriete propre, et il s'affiche dans le Views/Event.cshtml de Gilliard sans une ligne de Razor — uniquement parce qu'il compose scheduleFacts. C'est exactement ce que « N doctypes -> 1 template » promettait.
Pourquoi price/location restent en ligne sur chevaliersActivity : aucune autre marque n'a d'activites. Regle (cf. le contrat de tokens) : partage par 2+ marques -> composition ; une seule marque -> local. Si PDN en veut un jour, on promeut a ce moment-la. location porte le meme alias que dans scheduleFacts/vacancyFacts a dessein : childListing lit par alias, donc « lieu » veut dire la meme chose partout.
Les doctypes de liste existent pour contraindre l'arbre (<Structure>), pas pour porter des champs : seules des activites sous chevaliersActivityList, seuls des evenements sous chevaliersEventList. Ils pointent tous sur le template ContentPage et rendent leurs enfants via le bloc childListing.
Compositions de champs partagees (Phase 0.5)
Le pendant de <site>Canvas : le canvas partage les blocs, ces compositions partagent les champs. Ensemble, elles rendent les doctypes structures de chaque marque quasi vides.
| Composition | Cle | Onglet | Champs | Sert |
|---|---|---|---|---|
scheduleFacts | f1c33003-…0006 | Agenda | startDate, endDate (Date Picker with time), location, intro, ctaLabel, ctaUrl | event, puis chevaliersEvent, gilliardayArtist |
vacancyFacts | f1c33003-…0007 | Poste | contractType, percentage, location, department, applyEmail, closingDate, body | vacancy, puis les offres des autres marques |
articleFields | f1c33003-…0008 | Article | category, body, excerpt, author, publishDate | blogPost, puis pdnBlogPost |
L'extrait de la carte de listing - articleFields.excerpt + childListing.excerptWords
La carte d'une liste affiche un resume que WordPress resout en deux temps, et nous faisons pareil, parce que c'est mesure sur le live et non suppose :
excerpt(TextArea, surarticleFields) - l'extrait manuel de l'auteur. Il n'est PAS le debut du corps : sur PDN,/pink-watermelon/s'ouvre sur « Ingrédients » alors que sa carte lit « Le Pink Watermelon est un cocktail frais et fruité... ». Quatre articles PDN en portent un (les cocktails) ; aucun autre contenu des quatre marques.- le corps, sinon -
bodys'il est rempli, sinon le texte riche decontentBlocks. Ce second repli est indispensable : les articles PDN sont ecrits en blocs, leurbodyest volontairement vide, et lirebodyseul laissait les six listings sans aucun extrait.childListing.cshtmllit l'aliasbodyde chaque bloc - le meme contrat d'alias que le reste du fichier.
excerptWords (Numeric, sur le bloc childListing) donne la troncature. C'est une constante de theme WordPress, donc elle differe par marque : 16 mesures sur /blog/ de Gilliard, 30 sur chaque archive PDN. Vide = 16. Ce n'est pas un detail de confort : il n'y a aucun clamp CSS d'aucun cote, la hauteur de carte suit le texte, et un mot de trop vaut une ligne de 30.6px.
Resultat : vacancy et blogPost sont desormais des coquilles vides (zero <GenericProperty>). event ne garde que son contentBlocks en ligne (asymetrie Gilliard assumee, cf. ci-dessus).
Libelles neutres, obligatoire : ces compositions sont vues par les editeurs des 4 marques. Les onglets s'appellent « Agenda » / « Poste » / « Article », jamais « Contenu Gilliard ». Rien de specifique a une marque ne rentre ici.
gilliardayArtist n'aura aucun champ propre : nom/photo = pageHero, date/heure/scene/bio = scheduleFacts. C'est ce qui evite un doctype « artiste » sur mesure.
Alias en double assumes : location est dans scheduleFacts ET vacancyFacts ; body dans vacancyFacts ET articleFields. Sans danger tant qu'aucun doctype ne compose les deux — et si ca arrive, Umbraco le refuse bruyamment a l'enregistrement. Ne pas « corriger » par un prefixe.
⚠️ Deplacer une propriete vers une composition DETRUIT ses valeurs. Les valeurs sont liees au propertyTypeId ; changer de content type cree une nouvelle ligne et les anciennes valeurs tombent. Constate lors de la 0.5 : apres import, toutes les dates/lieux des evenements etaient vides. Sans danger ici parce que tout le contenu Gilliard est reproductible via /seed-pages — rejouer le seeder fait partie de la manoeuvre, pas d'un nettoyage optionnel. Sur du contenu saisi a la main, il faudrait exporter les valeurs avant.
Consommation cote Razor : voir le piege IPublishedElement dans ../architecture/patterns.md — ni Children<IScheduleFacts>(), ni UmbracoViewPage<IScheduleFacts>.
DataTypes partages (uSync/v17/DataTypes/, dossier backoffice Blocks/Core) — prefixe Core - : ils sont neutres vis-a-vis de la marque et servent les 4 sites. Le seul DataType reellement par-site est Gilliard - Content Blocks (dossier Blocks/Gilliard), qui definit quels blocs l'editeur Gilliard voit ; chaque marque aura le sien.
Core - Hero Slides(CoreHeroSlides.config) —Umbraco.BlockListdu blocheroSlide(proprieteslides).Core - Cards—Umbraco.BlockListdu bloccard(proprietecards).Core - Card Display— ButtonPicker (grid-2/grid-3/grid-4/products-3/products-5/bottles/carousel/list/steps/logos/facts), proprietedisplaydecardCollection. Remplace l'ancien coupleuseCarousel+columns: une seule valeur decrit l'affichage, elle sert directement de modificateur BEM (card-collection--<display>) et le nombre de colonnes en est derive (jamais saisi). Ajouter un affichage = 1 entree ButtonPicker + 1 bloc CSS, zero Razor.products-3vsproducts-5: les marques different pour de vrai — gilliard aligne 5 gammes, chevaliers 3 (live :.section-wine-selection-2 .col-wine-bottle{width:33.3334%}= 3-up, mesure 334px @1440). Le generateur derive la valeur du nombre de cartes.facts= le bandeau « En bref… » des pages activite chevaliers (live.section-agenda) : colonnescol-lg-2(16.667% de la rangee, pas du conteneur interieur) centrees, icone 40x40 dans une boite de 46, legende 20px/24px grise. Le nombre de colonnes vaut le nombre de faits (2 a 5 sur le live), donccols=cards.Countpour cet affichage.bottles= la bande vins de l'accueil PDN : bouteilles portrait contenues (ratio naturel 165/392) dans un panneau degrade (orange -> rose), 2/3/3/4 colonnes, avec l'overlay SVG grappe en haut a droite. Detecte par une signature mesuree (toutes les images de carte portrait dans leurs dimensions naturelles), pas par page.Core - Media Text Display(CoreMediaTextDisplay.config, Keyf1d22002-…-0015) — ButtonPicker (photo/feature/product/panel/duo), proprietedisplaydemediaText.duo= une PAIRE de portraits cote a cote dans la colonne media, alimentee parimage+secondImage(bande 2 de l'accueil PDN). Une seconde photo a sa PROPRE propriete plutot que de monter dans le sloticon, qui est un emblème et le dit dans son libelle - c'est la dette quepanelporte deja et que la phase 2 doit solder. ⚠️ Le generateur emet aussiwineetproduct-detail, qui ne sont pas dans ce ButtonPicker : un editeur ne peut pas les choisir. Voir le tableau des traitements plus haut. Ne jamais deduire la variante d'un autre champ.panel= la bande club de l'accueil PDN : photo a gauche (fond de colonne), panneau degrade rose->ambre a droite avec texte blanc et une image decoupee posee sur le coin bas-droit (elle occupe le sloticon).Core - Media Gallery Display— ButtonPicker (square-contained(defaut)/square-fullbleed/grid/collage), proprietedisplaydemediaGallery. Le defaut est CONTENU : mesure sur le live gilliard, la bande.section-insta-feedest dans un.containernormal (1350 @1440, 1736 @1920) avec des tuiles 326²/422.5² et un ecart de 10px.square-fullbleedne correspond a aucune marque mesuree ; il reste pour les collages de gilliarday (phase 3).Core - Hero Layout—photo/banner, proprietelayoutdeheroSlide.Core - Form Type—Umbraco.DropDown.Flexible(itemscontact/inquiry/booking/newsletter/activity), utilise par la proprieteformTypedu blocformBlock.activity= le formulaire des pages activite chevaliers : Nom + Prenom, puis Telephone + Email en rangees de 2, message de 322px, bouton de 300px — ni case de consentement ni adresse (ca, c'est le formulairecontact). Le bloc emet aussi un modificateurform-block--<type>, ce qui permet a chaque formulaire d'avoir sa geometrie sans toucher aux autres (safelist PurgeCSS/^form-block--/).Core - Team Groups—Umbraco.BlockListdu blocteamGroup(proprietegroupsdeteamGrid).Core - People—Umbraco.BlockListdu blocperson(proprietepeopledeteamGroup).Core - Main Menu/Core - Menu Links/Core - Social Links/Core - Social Network— menus et reseaux sociaux desiteSettings.Gilliard - Content Blocks—Umbraco.BlockGridexposant les 10 blocs de premier niveau (heroSlider,cardCollection,mediaText,richIntro,ctaBanner,formBlock,map,newsletterSignup,mediaGallery,teamGrid), tousallowAtRoot. Expose surhomePage,contentPageet les doctypes structures (venue/venueList/eventList/event/blogList/vacancyList) via la proprietecontentBlocks(onglet Contenu Gilliard), rendue parHtml.GetBlockGridItemsHtmlAsync(Model.ContentBlocks).
Bloc teamGrid (page Team) — grille d'equipe imbriquee sur deux niveaux : teamGrid.groups (BlockList de teamGroup) ; teamGroup = heading (TextBox) + people (BlockList de person) ; person = photo (MediaPicker3) + name + role (TextBox). Rendu Views/Partials/blocks/teamGrid.cshtml : intitule de departement (H2 or 32px/8px centre) au-dessus d'une grille 4-up de portraits carres (1:1) + nom or 19.2px/4.8px majuscules (live .section-team-info). La page Team = contentPage avec un bloc teamGrid + un ctaBanner (pas de doctype dedie).
Le bloc map utilise l'embed Google sans clé (maps.google.com/maps?q=…&output=embed, qui redirige vers /maps/embed — la forme www.google.com/maps?output=embed ne s'affiche PAS dans une iframe). Note consentement : cet iframe charge Google ; à passer derrière le consentement cookies quand le module sera finalisé.
Le bloc formBlock rend un formulaire front-end uniquement (champs fixes selon formType ; l'envoi sera cable plus tard, comme l'e-commerce) : styles alignes sur les CF7 du live (champs bordure or 0.8px, sans radius, placeholder or ; bouton d'envoi pleine largeur en contour or). Vue blocks/formBlock.cshtml, styles sections/_form-block.scss.
Doctypes structures (pages liste/detail, Folder=Pages) — tous composent pageHero (sauf textPage) + pageSettings. Templates Views/<Nom>.cshtml. Autorises en enfant de homePage.
⚠️ Les 4 doctypes de liste n'ont plus de template propre. BlogList/EventList/VenueList/ VacancyList.cshtml ont ete supprimes : ils ne differaient que par le tri et le traitement des cartes. Les 4 doctypes pointent desormais sur le template ContentPage et la liste est un bloc childListing dans leur contentBlocks. Consequence : ajouter une liste ne demande plus de code, et les 3 autres marques la reutilisent telle quelle.
⚠️ Piege: un noeud stocke son propre templateId, fixe a la creation. Repointer le doctype sur un autre template ne deplace pas les noeuds existants — ils continuent de viser l'ancien et 404 des qu'il est supprime. FindOrCreate (seeder) re-aligne donc le templateId sur le template par defaut du doctype a chaque passage.
Doctype (Key f1b22001-…) | Champs promus / propriete | Enfants | Template / rendu |
|---|---|---|---|
venueList (…0002) | contentBlocks (childListing layout=cards + CTA final) | venue | Template ContentPage. Banniere + cartes auto des venue (image heroImage, titre or 3:2) + grille |
venue (…0003) | contentBlocks (grille) | — | Banniere + blocs (richIntro + mediaText + formBlock booking + ctaBanner). Pas de champ capacity : la capacite est en prose sur le live |
textPage (…0004) | bodyText (RichText) ; pas de pageHero | — | Conteneur + titre (nom) + corps riche. Pages legales (CGV, confidentialite) |
blogList (…0005) | contentBlocks (childListing layout=cards-blog showCategoryFilter=true) | blogPost | Template ContentPage. Banniere + filtre categories (?category=slug) + cartes 4:3 avec pastille categorie or |
blogPost (…0006) | category,author (TextBox) ; body (RichText) ; publishDate (DatePicker) | — | Banniere + corps centre (col-lg-8, 900px) + articles lies (meme categorie, 3) |
eventList (…0007) | contentBlocks (childListing layout=cards order=agenda + CTA final) | event | Template ContentPage. Banniere + cartes auto triees par startDate (a venir puis passes) + grille |
event (…0008) | startDate/endDate (DatePicker), location,ctaLabel (TextBox), ctaUrl (MultiURLPicker), intro (RichText), contentBlocks | — | Banniere + faits (dates FR/lieu) + intro + blocs + bouton reservation externe |
vacancyList (…0009) | contentBlocks (childListing layout=links) | vacancy | Template ContentPage. Banniere + intitule souligne + liste de liens (.positions, 545px) vers les postes. listHeading supprime -> c'est le heading du bloc |
vacancy (…000a) | contractType/percentage/location/department/applyEmail (TextBox), closingDate (DatePicker), body (RichText) | — | Banniere + faits + corps + bouton « Postuler » (mailto applyEmail) |
Dates promues (event, vacancy, blogPost) via le DataType built-in DatePicker (5046194e-…, Umbraco.DateTime) pour tri/statut/import propre. Carte de liste partagee : Views/Partials/_listCard.cshtml (+ POCO Models/ListCard.cs) — reutilise le look .g-card en variante .g-card--listing (titre or majuscules) et .g-card--blog (image 4:3 + pastille). Note structurelle : sur le live les articles/postes sont a plat (/fr/<slug>) ; ici ils sont enfants de leur liste (/blog/<slug>, /emploi/<slug>).
Navigation & pied de page — composition siteSettings
Le header (_header.cshtml) et le footer (_footer.cshtml) sont pilotes par le contenu, pas en dur. Meme pattern que cookiesSettings/marketingSettings : une composition siteSettings (Key f1c33003-…-0002, IsElement=false, Folder=Compositions) est composee sur homePage. Les deux partials recoivent deja le HomePage type via Html.PartialAsync(..., homePage) depuis _MasterLayout -> ils lisent la nav sur Model (interface generee ISiteSettings) sans injection ni view component. Les valeurs vivent sur le noeud racine de chaque site : la nav est donc automatiquement par-site.
Element types (uSync/v17/ContentTypes/) :
menuItem(f1a11001-…-0015) :label(TextBox),link(MultiUrlPicker, optionnel),children(BlockListCoreMenuLinks->menuLink). = un item de premier niveau.menuLink(f1a11001-…-0016) :label(TextBox),link(MultiUrlPicker),columnHeading(TextBox, optionnel — sa presence sur au moins un enfant fait basculer le dropdown en mega-menu groupe par colonne),style(TextBox —promo/allpilotent.site-menu__promo/.site-menu__all). Reutilise pourmenuItem.children, le footer nav et les liens legaux du footer.socialLink(f1a11001-…-0017) :network(DropdownCoreSocialNetwork: facebook/instagram/linkedin,Variations=Nothing),url(TextBox). Le SVG de l'icone est choisi cote vue (SvgFor(network)dans_footer.cshtml).
DataTypes (uSync/v17/DataTypes/) : CoreMainMenu (…0007, BlockList -> menuItem), CoreMenuLinks (…0008, BlockList -> menuLink, useInlineEditingAsDefault), CoreSocialLinks (…0009, BlockList -> socialLink), CoreSocialNetwork (…000a, Umbraco.DropDown.Flexible).
Proprietes de siteSettings (onglets Marque + Navigation + Pied de page) :
- Marque :
deSiteUrl(TextBox, invariant) — cible de la pastille DE de l'en-tete tant que le contenu allemand n'est pas heberge ici. Le live affiche deux pastilles (fr + de) sur les deux marques ; nous n'hebergeons que le FR, donc la pastille pointe vers le site allemand du client (https://gilliard.ch/de/,https://chevaliers.ch/— hrefs mesures). Vide = pas de pastille. ⚠️ Ne jamais coder l'URL en dur dans_header.cshtml: cette partial sert les quatre marques. - Marque :
brandName(TextBox, invariant — meme nom dans toutes les langues). Nom du site, ex. « Maison Gilliard ». Sert dealtdu logo, de libelle d'accessibilite et de valeur par defaut du titre de contact du pied de page. Aucun nom de marque ne doit etre code en dur dans une vue : quatre marques partagent_header/_footer/map. - Navigation :
mainMenu(BlockListCoreMainMenu). - Pied de page :
footerMenu+footerLegalLinks(BlockListCoreMenuLinks),socialLinks(BlockListCoreSocialLinks),footerContactHeading/footerPhone/footerEmail/footerSecondaryPhone/footerSecondaryPhoneLabel/newsletterHeading/newsletterLead/newsletterConsent/footerCopyright(TextBox),footerAddress(TextArea).
footerCopyright(Keyc1c30002-…-000f, ajoute le 2026-07-17) — la ligne de la barre du bas. L'element de dictionnaireFooter.Copyrightsest partage par les 4 marques : il ne peut donc pas porter la raison sociale de chacune.siteSettingsgagne quand il est rempli, le dictionnaire reste le repli (defaut du gabarit / marques pas encore migrees).{year}est substitue par_footer.cshtmldans les deux cas — le live affiche une annee dynamique, donc on seme le jeton et non « 2026 », sinon la ligne perime au 1er janvier. Semes depuis le live : gilliard « Copyright © {year} Maison Gilliard. », chevaliers « Copyright © {year} Domaines Chevaliers SA. All rights reserved. » Le credit d'agence reste Spektrum (le live credite tokiwi SA) : ecart volontaire, voirassets-substitues.md§6bis.
footerSecondaryPhone(ex-footerVinothequePhone, renomme le 2026-07-16 — « Vinotheque » est du vocabulaire Gilliard qui fuyait vers les trois autres marques). Le libelle affiche devant le numero vient desormais defooterSecondaryPhoneLabel(Gilliard : « Vinotheque » ; vide = aucun libelle). La cle de proprietec1c30002-0000-4000-8000-000000000009a ete conservee : les valeurs sont stockees parpropertyTypeId, uSync matche par<Key>et renomme l'alias sur place -> la donnee survit. Changer la cle aurait supprime la valeur silencieusement.
Rendu header : chaque menuItem sans enfant -> <li class="site-menu__item"> simple ; avec enfants -> site-menu__item--has-panel ; si un enfant porte un columnHeading -> panneau .site-menu__mega (children.GroupBy(columnHeading)), sinon simple .site-menu__sub. Le selecteur de langue est genere depuis Umbraco.AssignedContentItem.Cultures (actif = GetCultureFromDomains()). Compte/panier restent des placeholders statiques (e-commerce hors scope). Comportement 100 % CSS (hover + checkbox mobile) — les classes existantes sont preservees.
Liens internes vs externes (choix produit) : les entrees dans le scope de la migration pointent vers nos noeuds internes (URL resolue) ; les entrees hors scope (produits du mega Vins, Valloton, Chevaliers, promos, rencontres exterieures) pointent vers les URLs absolues du live gilliard.ch. Aucun lien laisse en #. Le seed (SeedSiteSettings dans DevSeedPagesController, voir extensibility.md) reproduit la nav complete du live.
Statut : ContentTypes uSync (8 elements) + DataTypes (
CoreHeroSlides,CoreCards,GilliardContentBlocks) crees ;homePage.contentBlockscable ; modeles ModelsBuilder generes (umbraco/Models/*.generated.cs) ; vues Razor build-verifiees (compilation complete OK). Contenu Home seede + publie viaGET /seed-home(DEBUG, voirextensibility.md) : hero, hero-boxes, Nos Gammes, Nos Activites, 2 mediaText, newsletter, instagram — media importes, rendu verifie (Playwright). Regeneration des modeles apres edition uSync : voir../domain/workflows.md.Phase A pages interieures (2026-07-15) — FAIT + verifie @1440 : composition
pageHero, blocctaBanner, doctypecontentPage(+ templateViews/ContentPage.cshtml),mediaTextpasse au traitement live (centre +icon+ carre). PageNos valeursseedee viaGET /seed-pages(DEBUG) et pixel-diffee contre le live (hauteur 5638 vs 5839 px - ANCIENNE barre @1440, a repasser au harnais 5 largeurs; cf.MIGRATION-PROMPT.mdsection 1b).Phase B (2026-07-15) — FAIT + verifie @1440 : toutes les pages libres
contentPage(contact + map + formBlock, en-primeur, a-propos, activites x2, ambassadeurs, vinotheque, newsletter, gammes, cochetta) ; blocsmap,formBlock(contact/inquiry/booking/newsletter), variantescard(activite/info) +mediaText --product,.job-list.Phases C-E (2026-07-15) — FAIT + verifie @1440 : doctypes structures
venueList/venue,textPage,blogList/blogPost,eventList/event,vacancyList/vacancy+ blocteamGrid(page Team =contentPage). DataTypesCoreTeamGroups/CorePeople. Un seul cycle de regeneration ModelsBuilder pour les 12 modeles. Seedes via/seed-pages(nodes 1260-1278) et verifies (mesure + captures) : team-grid, cartes de liste, filtre blog par categorie, dates FR promues (event/vacancy), articles/postes lies.Nav wiring (2026-07-15) — FAIT + verifie @1440 : composition
siteSettings(+ elementsmenuItem/menuLink/socialLink, DataTypesCoreMainMenu/CoreMenuLinks/CoreSocialLinks/CoreSocialNetwork) composee surhomePage;_header.cshtmlet_footer.cshtmlreecrits pour rendre depuisModel(HomePage : ISiteSettings). SeedSeedSiteSettings(node 1105) reproduit la nav live (mega Vins, dropdowns, footer nav/socials/ legaux/contact/newsletter). Verifie Playwright : hrefs internes pour le scope, live pour le hors-scope, mega 3 colonnes, footer pilote par contenu, burger mobile OK. Reste : affinage responsive 1024/768/375 ; e-commerce (vins/panier) hors scope.
Entites reutilisables - la Bibliothèque par marque (QA editorial, 2026-07-29)
Avant cette passe, tout le modele etait "en texte" : l'auteur d'un article, sa categorie, le lieu d'un evenement, les salles du formulaire de reservation etaient des Umbraco.TextBox ou, pire, des <option> en dur dans le Razor. Le modele ne contenait qu'un seul Content Picker (childListing.source) et aucun DataType MultiNodeTreePicker.
Consequences concretes, toutes constatees :
articleFields.authoretait un champ libre qu'aucune vue ne lisait : l'editeur pouvait le remplir, rien ne s'affichait nulle part.childListing.filterCategoryetait une liste separee par virgules comparee par chaine accent-repliee : une faute de frappe vidait la liste en silence.- Les 5 salles du formulaire de reservation (
Salle des Foudres,Clos du Mont, ...) etaient ecrites en dur dansformBlock.cshtmlalors que les memes lieux existent deja en tant que noeudsvenue.
La structure
Un doctype conteneur library (Bibliothèque, Folder=Containers, sans template donc sans adresse), autorise sous chaque racine de marque. Il contient :
| Alias | Role | Libelle affiche |
|---|---|---|
authors / author | conteneur + fiche | le nom du noeud est le nom affiche |
articleCategories / articleCategory | conteneur + fiche | le nom du noeud est le libelle de la pastille et du filtre |
author porte authorRole, authorPhoto, authorBio, authorLink. articleCategory n'a aucune propriete : renommer la page renomme la categorie partout. Les deux conteneurs ont une List View (ListViewContent), jusque-la inutilisee.
Les pickers - dynamicRoot, jamais une cle de noeud
Core - Picker - Auteur / - Categorie / - Categories (Folder=Pickers) sont des Umbraco.MultiNodeTreePicker coeur d'Umbraco (pas de Contentment : ce depot part en fork chez les projets aval, cf. regle 7). Chacun est ancre ainsi :
"startNode": {
"type": "content",
"dynamicRoot": {
"originAlias": "Site",
"querySteps": [
{ "alias": "NearestDescendantOrSelf", "anyOfDocTypeKeys": ["<library>"] },
{ "alias": "NearestDescendantOrSelf", "anyOfDocTypeKeys": ["<authors>"] }
]
}
}⚠️ originAlias: "Site" est ce qui rend le picker multi-marques. Il resout la racine du site du noeud en cours d'edition, donc un editeur Gilliard ne voit jamais les auteurs de Chevaliers, sans une ligne de configuration par marque. Une startNode.id en dur aurait fait exactement l'inverse - et les ids different par environnement (cf. le piege des racines dans extensibility.md).
author sur articleFields est passe de Umbraco.TextBox a Core - Picker - Auteur (maxNumber: 1), donc ModelsBuilder emet IPublishedContent Author, pas une collection. BlogPost.cshtml rend enfin une signature - uniquement si l'article pointe un auteur, et rien n'est seede : aucune page mesuree du live n'affiche de signature, donc le rendu reste 1:1 et la fonctionnalite est la le jour ou le client la veut.
Reste a faire :
articleFields.categoryetchildListing.filterCategorysont encore des TextBox. Les basculer sur les pickers demande de changer en meme temps le filtrage dechildListing.cshtml(comparaison par cle de noeud au lieu de chaine), les pastilles de_listCard.cshtml/BlogPost.cshtml, et les seeders des 4 marques. A faire d'un bloc, pas a moitie : a moitie, le filtre du blog casse.
locationreste volontairement un TextBox. Le brancher sur un picker devenuedonnerait un champ vide sur 3 marques sur 4 (seul Gilliard a desvenue), donc une regression par rapport au champ libre. A reprendre le jour ou les lieux vivent aussi dans la Bibliothèque.
Blocs editoriaux ajoutes par la passe QA (2026-07-29)
| Bloc | Element | Proprietes | Pourquoi |
|---|---|---|---|
| Questions frequentes | faqList (icon-help) | pretitle, title, items -> Core - FAQ Items | Les FAQ etaient ecrites a la main dans le RTE (<h2>FAQ</h2> + questions en gras, cf. Seed/GilliardPostData.cs). Aucun element type FAQ n'existait. |
faqItem (icon-help-alt) | question (TextBox, obligatoire), answer (RTE, obligatoire) | Meme forme que accordionElement de metiers-d-art. | |
| Fait | factItem (icon-list) | label (obligatoire), value (obligatoire), icon | Les lignes de faits etaient 6 proprietes fixes (productFacts) dont les libelles etaient en dur dans _pageFacts.cshtml : ajouter une 7e ligne demandait un doctype et du Razor. |
faqList est rendu par Views/Partials/blocks/faqList.cshtml en <details>/<summary> natifs : pas de JavaScript, operable au clavier, trouvable par la recherche du navigateur. Style dans styles/base/sections/_faq.scss (tokens de theme, donc chaque marque le colore gratuitement).
faqList est aussi autorise dans le Rich Text (RichtextEditor.config, dont blocks etait [] alors que Umb.Tiptap.Toolbar.BlockPicker etait deja dans la barre d'outils) : une FAQ peut donc vivre au milieu d'un corps d'article, ce qui est exactement le cas d'usage qui produisait la FAQ en prose.
Les 4 grilles de marque - taxonomie partagee et apercus UFM
Trois corrections d'un coup sur Gilliard/Chevaliers/PDN/Gilliarday - Content Blocks :
- Les 44 libelles etaient statiques (
"Media + Texte"), donc dix blocsmediaTextsur une page s'affichaient comme dix lignes identiques. Ils portent desormais un apercu UFM :Média + texte: {=title | fallback:"(sans titre)"}. Le type reste visible avant le deux-points. - Les groupes ne groupaient rien : chaque grille avait un seul groupe, nomme comme la marque. Quatre groupes partages (memes GUID partout) les remplacent, et chaque bloc porte son
groupKey: Bandeaux / Contenu éditorial / Listes et collections / Formulaires et widgets. - Les jeux de blocs avaient derive :
mediaGallerymanquait a Chevaliers,teamGrid+embeda PDN,teamGrida Gilliarday. Rien ne le justifiait - le rendu est partage (meme partial, styles dansbase/), donc les 4 marques peuvent tout rendre. Les 4 grilles exposent maintenant les memes 13 blocs.
Ajouter / modifier un document type
Backoffice -> uSync exporte (ExportOnSave="Settings") ; les modeles se regenerent (SourceCodeAuto). Voir ../architecture/patterns.md.
Avant de valider : vérifier l'icône (section ci-dessus) et respecter les conventions de rédaction pour les libellés vus par l'éditeur (../domain/conventions-redaction.md).
Reconciliation avec l'export WXR (audit, 2026-07-27)
Le contenu des deux marques (gilliard, chevaliers) est seede depuis un scrape du live (tools/seed-gen/*-bands.py/bands.json, controllers DevSeed*), PAS depuis l'export WXR. L'export a ete extrait vers tools/seed-gen/data/sources/<brand>/{pages,media,people,posts,menu}.json (git-ignore) mais un audit cible (current seed vs export) montre qu'il ne faut PAS seeder brut depuis l'ACF :
- L'ACF de l'export contient des field-groups masques / non rendus par les templates du live, et des entrees CPT supplementaires non affichees. Exemples MESURES :
company_info_info_box[5]surdomaines-chevaliers("A TRAVERS LE TEMPS", "UN SOL POUR CHAQUE VIN"...) est absent de la page rendue du live (verifie en direct) ;people.json= 105 personnes alors que le live/seed en affichent 54 ;posts.json= 9 articles alors que le blog en affiche 6. - Le seed base sur le scrape reproduit exactement ce que le live affiche : compteurs alignes (chev home 6 experience-boxes = hero-boxes 3 + activites 3 ; gammes 3/5 ; domaines team 5 ; contact 2), copie legale (
body) identique octet pour octet. - Regle : seeder brut depuis
pages.jsonAJOUTERAIT du contenu que le live ne montre pas => une regression 1:1. L'export sert a confirmer le verbatim + les ids media exacts, pas de source de seed directe. Si une provenance-export est voulue plus tard (editabilite), il faut filtrer l'ACF aux field-groups reellement rendus par template, pas seeder l'integralite.
Blocs et cultures - la regle qui rend une page vide sans rien casser (mesure 2026-08-17)
Un Block Grid ou un Block List pose sur une propriete dont Variations vaut Culture stocke, dans sa valeur JSON, la culture a laquelle chaque bloc appartient, a deux endroits :
{
"contentData": [{ "values": [{ "alias": "titre", "culture": "de", ... }] }],
"expose": [{ "contentKey": "...", "culture": "de", "segment": null }]
}exposedecide si le bloc existe dans cette culture. Culture fausse -> aucune bande n'est peinte.contentData[].values[].culturedecide si les valeurs du bloc existent. Culture fausse -> les bandes sont peintes mais vides.
Le piege : les deux niveaux acceptent null sans erreur. La valeur est enregistree, publiee, et s'affiche normalement dans le backoffice - seul le rendu de la culture non par defaut est vide. Aucune exception, aucun log, rien dans le HTML qui le signale. Mesure : chevaliers /de/vinothek/ rendait 11,3 Ko de chrome contre 23,7 Ko pour la page francaise, avec le contenu allemand bien present en base.
Cote seeders, la culture se passe explicitement :
n.SetValue("contentBlocks", Seed.Grid(blocs, DeCulture), culture: DeCulture); // Block Grid
root.SetValue("mainMenu", Seed.ListIn(DeCulture, blocs), culture: DeCulture); // Block ListSeed.Grid(blocs) et Seed.List(...) sans culture restent corrects pour la langue par defaut et ne changent pas - c'est ce qui rend l'ajout retro-compatible.
Symptome a reconnaitre : une page traduite qui rend son titre, son fil d'Ariane et son chrome, mais pas une seule bande. Ce n'est ni le routage, ni la publication, ni le doctype : c'est la culture dans la valeur du bloc.

