Skip to content

Points d'extension Umbraco

Le coeur du template. Tous les enregistrements passent par SiteComposer ; les comportements techniques (404, 500, sitemap, robots, login dev) sont des controllers ou des content finders.

Composer (src/Web/SiteComposer.cs)

IComposer unique, decouvert par AddComposers() dans Program.cs. Il :

  • builder.SetContentLastChanceFinder<NotFoundContentFinder>() ;
  • enregistre le service du dashboard "Système" : builder.Services.AddSingleton<IDashboardInfoService, DashboardInfoService>() ;
  • enregistre le provider 2FA : new BackOfficeIdentityBuilder(builder.Services).AddTwoFactorProvider<UmbracoUserAppAuthenticator>(UmbracoUserAppAuthenticator.Name).

C'est le point d'entree pour tout nouveau wire-up (DI, notification handlers, content finders...).

Resolution du site courant (Services/SiteResolver.cs) — multi-sites

ISiteResolver.GetRootAsync(authority, path) : unique point de resolution host (+ path) -> racine du site. Enregistre en singleton dans SiteComposer.

Cette instance heberge plusieurs marques cote a cote (une racine *Home par marque + un domaine). Avant, quatre endroits refaisaient ce lookup a la main et retombaient chacun sur "la premiere racine trouvee" : inoffensif avec un seul site, silencieusement faux avec quatre (une requete pour la marque A pouvait recevoir la 404 / la 500 / le sitemap de la marque B).

Regles de match :

  • DomainName accepte les formes exemple.ch, https://exemple.ch:44360, exemple.ch/fr (path-scoped — deux cultures sur un meme host, prevu pour chevaliers.ch/fr) et /fr (path seul, tous hosts).
  • Le scheme est retire, la valeur est Trim()ee (des lignes umbracoDomain contiennent des espaces parasites).
  • Priorite : domaine avec host avant un domaine path-seul, puis path le plus long (chevaliers.ch/fr gagne sur chevaliers.ch pour /fr/contact).
  • Aucun fallback arbitraire : host inconnu -> null, l'appelant decide (en pratique 404). Servir le contenu d'une autre marque n'est jamais la bonne reponse.

Consommateurs : NotFoundContentFinder, SitemapController, ErrorController, _MasterLayout.cshtml.

Cote vue : Model.AncestorOrSelf(1), JAMAIS AncestorOrSelf<HomePage>()

⚠️ Piege multi-marques, deja tombe une fois. _MasterLayout resolvait la racine ainsi :

cshtml
var homePage = Model.AncestorOrSelf<HomePage>()
    ?? Umbraco.ContentAtRoot().OfType<HomePage>().FirstOrDefault();  // <- BUG

Seul Gilliard utilise le doctype homePage. Pour une page de chevaliers/pdn/gilliarday la recherche typee renvoie null, et le repli « premiere racine » attrape la racine Gilliard : les 4 hotes servaient alors le theme, le logo et les menus de Gilliard. Constate le 2026-07-16 des la creation des racines par marque — chevaliers.localtest.me affichait « Maison Gilliard ». Aucune erreur, aucun log : juste la mauvaise marque.

Regle : racine de site = ancetre de niveau 1, resolu sans type :

cshtml
var siteRoot = Model.AncestorOrSelf(1);
var siteSettings = siteRoot as ISiteSettings;   // toutes les *Home composent siteSettings
var marketing = siteRoot as IMarketingSettings;
var theme = siteRoot.GetThemeName();

Si siteRoot est null (page hors arbre), on n'affiche pas l'en-tete/le pied plutot que de servir une autre marque — meme principe que SiteResolver : aucun repli arbitraire.

Page 404 par site (ContentFinders/NotFoundContentFinder.cs)

IContentLastChanceFinder (constructeur primaire : ISiteResolver). Logique :

  1. Resout la racine du site via ISiteResolver (host + path de la requete). Pas de racine -> false.
  2. Cherche un enfant de la racine dont le content type est NotFoundPage.ModelTypeAlias.
  3. Si trouve, SetPublishedContent ; retourne true si un contenu a ete pose.

Permet une page 404 differente par site dans une instance multi-sites.

Erreurs 500 (Controllers/ErrorController.cs)

Route ~/error/ (declaree dans ReservedPaths), branchee via UseExceptionHandler("/error") (Program.cs, hors dev). Si le status est 500, redirige vers le noeud internalServerErrorPage de la marque demandee (racine resolue par ISiteResolver) ; sinon redirige vers /.

Sitemap (Controllers/SitemapController.cs)

Controller simple (PAS RenderController), route /sitemap, ResponseCache 600 s. Construit un sitemap XML unique multilingue :

  • resout la racine via ISiteResolver ; la racine resolue est le noeud d'accueil ;
  • parcourt recursivement les enfants ;
  • n'inclut un noeud que s'il a un url segment, un template (vraie page) et n'est pas masque du sitemap (HideFromSiteMap) ; force https ; lastmod au format W3C.

Deux pieges corriges ici (le sitemap etait casse — 404 — avant) :

  1. Ne jamais heriter de RenderController pour une route custom. Umbraco route les RenderController via son propre pipeline de contenu (UmbracoRouteValues) : une action en attribute-routing dessus n'est jamais atteinte, et /sitemap servait silencieusement la page 404. RobotsController est un Controller simple pour la meme raison. Ce controleur retourne un ContentResult et ne rend aucune vue : il n'a besoin de rien de RenderController.
  2. Ne pas chercher HomePage.ModelTypeAlias dans les descendants. Chaque marque a son propre doctype d'accueil (chevaliersHome, ...) : ce filtre renvoyait null et 404ait le sitemap de toutes les nouvelles marques.

robots.txt (Controllers/RobotsController.cs)

Route /robots.txt, cache 24 h. En production : sert uniquement la ligne sitemap: (URL absolue https du sitemap). Hors production : user-agent: * + disallow: / (blocage indexation).

Login dev (Controllers/DevLoginController.cs)

#if DEBUG uniquement. Route GET /l. Double garde : compile en DEBUG ET check runtime IWebHostEnvironment.IsDevelopment() (sinon 404). Recupere jusqu'a 500 utilisateurs, prend le premier dont l'email finit par @spektrummedia.com, le connecte (non persistant) et redirige vers /umbraco. Ne jamais retirer une des deux gardes.

Regeneration des modeles dev (Controllers/DevModelsController.cs)

#if DEBUG uniquement. Route GET /generate-models. Meme double garde que le login dev (compile DEBUG + IsDevelopment(), sinon 404). Force ModelsBuilder a (re)generer les umbraco/Models/*.generated.cs a la demande, en appelant le meme generateur que le dashboard ModelsBuilder du backoffice.

Pourquoi c'est necessaire : le mode SourceCodeAuto ne regenere les modeles que sur une notification de sauvegarde d'un content type. uSync supprime ces notifications pendant son import au demarrage : un content type ajoute/modifie uniquement via les fichiers uSync/ ne declenche donc aucune regeneration. Sans cet endpoint, il faudrait ouvrir et re-sauver chaque type dans le backoffice. Le generateur (IModelsGenerator) est internal a Umbraco et son namespace change selon la majeure, donc il est localise par balayage des assemblies Umbraco* chargees (type exposant un GenerateModels() sans parametre) puis resolu via le conteneur DI. Voir le parcours dev dedie dans ../domain/workflows.md.

Seed racines de site dev (Controllers/DevSeedRootsController.cs)

GET /seed-roots (DEBUG + Development uniquement, comme DevLoginController). Met chaque marque sur son doctype de racine (chevaliersHome/pdnHome/gilliardayHome) et lie ses domaines. Idempotent : relancer signale « already ... — untouched ».

Pourquoi supprimer/recreer : le doctype d'un noeud ne peut pas etre change. Les 3 placeholders avaient ete crees en homePage (le doctype de Gilliard).

Garde-fou : refuse de supprimer une racine ayant le moindre enfant (CountChildren, verifie a chaud). Gilliard n'est pas dans la liste Roots et serait stoppe par ce test (18 enfants). Vide ensuite la corbeille pour qu'un placeholder ne soit pas restaure sur le mauvais doctype.

⚠️ Les domaines sont en base uniquement — uSync ne les serialise pas, ils ne partent pas vers staging/prod avec le commit. /seed-roots les recree selon l'environnement (DomainsFor), plus de ressaisie a la main :

environnementorigines liees
Developmenthttps://<marque>.localtest.me:44360 et http://<marque>.localtest.me:12953 (les deux ports), plus un raccourci par chemin sur localhost : / Gilliard, /chevaliers, /novembre, /gilliarday
Staginghttps://<marque>.staging.spektrum-suisse.ch

Le label d'hote est le meme partout (gilliard, chevaliers, portedenovembre, gilliarday) ; chaque marque a tete allemande recoit en plus <origine>/de en de. Gilliarday reste FR seul.

Les hostnames sont listes en premier et restent l'origine canonique en dev : c'est ce que mesure tools/fidelity et ce vers quoi les liens seedes doivent resoudre. Le raccourci localhost/<marque> sert a naviguer sans hostname ; ne pas lancer un seeder depuis lui, un lien seede pourrait se figer en /novembre/.... Il utilise IDomainService.UpdateDomainsAsync (l'API courante ; Save/Delete/GetAssignedDomains sont obsoletes en 17) : elle remplace les affectations du noeud, d'ou l'idempotence. DefaultIsoCode remplace l'ancienne ligne joker *<id>.

Plomberie de seed partagee (Seed/)

Trois fichiers, DEBUG uniquement, qui rendent les seeders reutilisables par les 4 marques :

  • Seed/ElementTypes.cs — les Keys des element types du jeu de blocs partage. Source unique. Elles etaient dupliquees en constantes privees dans chaque controleur de seed : c'est comme ca qu'un bloc finit seede sous deux noms, ou qu'un controleur continue de seeder une Key qui n'existe plus apres un renommage.
  • Seed/SeedContext.cs — quelle marque une execution vise : RootId, RootDocType, ContentPageAlias, Culture, MediaFolder. La table SeedContext.Brands est la source unique du cablage par marque (les fabriques Gilliard()/Chevaliers()/… et les tests la lisent, plutot que deux copies tenues a la main).
  • Seed/SeedSession.cs — la mecanique agnostique de marque : FindOrCreate, SetHero, PublishNode, Grid, SeedPage, SeedText, InternalLink. Un controleur de site ne garde que son contenu (les textes, la liste de medias, la structure des blocs) et delegue le reste.

⚠️ Ne JAMAIS resoudre une racine par id de noeud. GilliardHomeId = 1105 etait code en dur. Les ids different par environnement — et ne sont meme pas stables en local : recreer les racines placeholder sur leurs propres doctypes les a fait passer de 1106/1107/1108 a 1382/1383/1384. On resout par alias de doctype de racine (SeedContext.Resolve -> ContentService.GetRootContent()), le seul point d'ancrage stable, et chaque marque n'a qu'une racine donc l'alias identifie le site. Aucun repli : racine introuvable -> on le signale, on ne seede pas par-dessus une autre marque.

Chaque marque a son propre dossier media (Gilliard Seed, Chevaliers Seed, …), sinon l'import d'un seeder ecrase celui d'un autre. Verrouille par test.

Ajouter une marque = 1 ligne dans SeedContext.Brands + un controleur qui ne contient que son contenu.

Reinitialiser et rejouer le seed (DevSeedAllController, DevSeedClearController, Seed/SeedCleaner.cs)

Deux endpoints d'orchestration, memes trois verrous que les autres seeders (#if DEBUG || ALLOW_SEED, 404 hors Development/Staging, login backoffice).

  • GET /seed-all rejoue les 5 seeders dans l'ordre de dependance : /seed-roots, puis /seed-home, /seed-pages, /seed-chevaliers, /seed-pdn. Parametres : clear=yes (nettoie d'abord), brand= et media= (passes a l'etape de nettoyage), only=seed-pdn,seed-pages pour n'en rejouer qu'une partie. Le rapport est stream, un flush par etape : une passe complete telecharge plusieurs centaines d'images et dure des minutes, assez pour qu'un reverse proxy coupe une connexion inactive - et assez pour qu'un seul bloc de texte a la fin n'apprenne rien pendant l'attente. Les sous-seeders sont instancies depuis le conteneur DI (ActivatorUtilities.CreateInstance) et leur action est attendue en direct : leurs constructeurs ne prennent que des services et aucun ne touche HttpContext ni Url, donc l'appel en process equivaut aux 5 appels HTTP sans les allers-retours. Leur propre garde Development/Staging s'execute quand meme.
  • GET /seed-clear?confirm=yes supprime le contenu seede. confirm=yes est obligatoire : c'est le seul endpoint destructif de la famille, et un GET nu est exactement ce que produisent un favori, un prefetch de navigateur ou une URL autocompletee. Sans le parametre, il n'affiche que son mode d'emploi. Parametres : brand=<gilliard|chevaliers|pdn|gilliarday|all> et media=yes pour vider aussi le dossier media de la marque (le run suivant re-telecharge tout).

Ce que le nettoyage supprime : tous les enfants de chaque racine de marque, branche comprise (IContentService.Delete est definitif, il ne remplit pas la corbeille), et en option le dossier media <Marque> Seed.

Ce qu'il garde volontairement : les 4 racines de site. Leur doctype, leurs cultures et surtout leurs domaines ne sont pas du contenu de seed - les domaines sont en base uniquement (uSync ne les serialise pas) et c'est /seed-roots qui les recree, par environnement. Supprimer une racine jetterait ce reglage et rendrait le site en 404 jusqu'au prochain /seed-roots. La corbeille non plus n'est pas touchee par le nettoyage - mais /seed-roots la vide a chaque passage, pour sa propre raison.

Pourquoi il existe : les seeders sont idempotents sur (parent, alias, nom). Cela couvre une reexecution, pas un renommage ni une page qui cesse d'etre seedee - l'ancien noeud reste et le site porte les deux (c'est le defaut qui a donne douze cartes de lieux la ou le live en a dix, et qui a motive SeedSession.PruneChildren). En local, la remise a zero etait "supprimer le fichier SQLite et repartir" ; en staging la base est reelle, donc la meme remise a zero passe par un endpoint.

Seed contenu Home dev (Controllers/DevSeedController.cs)

#if DEBUG uniquement. Route GET /seed-home. Meme double garde. Peuple la propriete contentBlocks (Block Grid) de la racine Gilliard (node 1105) avec un contenu representatif puis publie la culture fr, pour permettre le pixel-diff sans saisie manuelle au backoffice. Idempotent (ecrase + republie). Construit la valeur block-editor JSON (format v14+ : contentData / expose / layout.Umbraco.BlockGrid, listes imbriquees en layout.Umbraco.BlockList) et l'affecte via IContentService.SetValue + publication via IContentPublishingService.PublishAsync (l'ancien SaveAndPublish a ete retire en Umbraco 17). Sert aussi de prototype du futur script d'import WordPress. Increment actuel : blocs textuels (TextBox / TextArea / BlockList imbriquee) ; media (MediaPicker3), texte riche (RichText) et liens (MultiUrlPicker) a ajouter ensuite.

Convention de liens CTA (home) : les CTA « vins/boutique » (hors scope e-commerce) pointent vers le live absolu https://gilliard.ch/fr/vins/ ; les CTA vers du contenu dans le scope pointent vers nos pages internes relatives (evenements -> /evenements/, activites -> /activites-oenotouristiques/). Ne jamais seeder un lien interne vers un slug inexistant (/vins/, /agenda/, /oenotourisme/ n'existent pas -> 404).

Seed pages interieures dev (Controllers/DevSeedPagesController.cs)

#if DEBUG uniquement. Route GET /seed-pages. Meme double garde. Seede plusieurs pages contentPage sous la racine Gilliard (node 1105) via un helper SeedPage(name, heroTitle, heroImage, blocks) : Nos valeurs (richIntro + 6 mediaText + ctaBanner), En primeur (richIntro + 2 mediaText + formBlock inquiry + ctaBanner) et Contact (cardCollection de 3 cartes info + map + formBlock contact étroit + ctaBanner). Importe les media du client dans le dossier "Gilliard Seed", renseigne la composition pageHero puis la grille contentBlocks, publie fr. Idempotent. Meme format de valeur block-editor et memes builders que DevSeedController, factorises dans DevSeedSupport (voir plus bas). Prototype d'import page-par-page ; ajouter une page = definir ses blocs + un appel SeedPage.

Depuis les phases B/C-E, /seed-pages seede aussi toutes les pages libres (a-propos, activites x2, ambassadeurs, vinotheque, newsletter, gammes, cochetta) et les pages structures : Team (contentPage avec bloc teamGrid imbrique groupes/personnes), Lieux d'accueil (venueList + 3 venue), CGV/Confidentialite (textPage), Blog (blogList + 3 blogPost), Evenements (eventList + 3 event avec dates promues), Emploi (vacancyList + 3 vacancy). Helpers generiques ajoutes : FindOrCreate(parent, alias, name), SetHero, PublishNode, Grid(blocks), SeedText. Les listes sont publiees avant leurs enfants (contrainte de publication Umbraco). Le block-list imbrique 2 niveaux (teamGrid.groups -> teamGroup.people) est construit via l'helper List(...) recursif.

Depuis le nav wiring, /seed-pages appelle aussi SeedSiteSettings() : peuple la composition siteSettings sur le noeud racine 1105 pour reproduire la nav du live (mega Vins, dropdowns Oenotourisme/Agenda/Nos domaines/A propos, items plats Vinotheque/Contact ; footer nav/socials/liens legaux/contact/newsletter). Helpers locaux : Item(label, link, children), Lnk(label, link, column, style), Social(network, url). Les liens dans le scope sont resolus vers nos pages internes via InternalLink(nodeId, name) (utilise IPublishedUrlProvider.GetUrl(...) et emet une valeur MultiUrlPicker de type externe avec l'URL relative resolue — rendu fiable, sans dependance au format JSON de lien interne) ; les liens hors scope pointent vers les URLs absolues du live (const string live = "https://gilliard.ch/fr/"). Puis PublishNode(1105, fr).

Plomberie partagee (Controllers/DevSeedSupport.cs)

#if DEBUG uniquement. Classe statique + records top-level partages par les deux seeders pour ne pas dupliquer ~130 lignes (nettoyage duplication 2026-07-16). Contient : les records Block(contentTypeKey, values) et PropertyValue(editorAlias, value) ; les constantes GilliardHomeId=1105, Culture="fr", MediaFolderName="Gilliard Seed", Ua ; ImportMediaAsync(...) statique (prend les services Umbraco en parametres — IMediaService, MediaFileManager, MediaUrlGeneratorCollection, IShortStringHelper, IContentTypeBaseServiceProvider, la liste des sources) ; et les builders de valeur par editeur Text/TextArea/Bool/Dropdown/Rich/Link/Media/MediaMultiple/List + l'assemblage BuildBlockEditorValue(blocks, layoutKey, grid) (format block-editor v14+). Les deux controllers font using static Web.Controllers.DevSeedSupport; pour garder des sites d'appel non qualifies (Text(...), Rich(...), List(...)). Chaque controller ne conserve que son orchestration propre (DevSeedController : les blocs home ; DevSeedPagesController : SeedPage, Grid, SeedText, PublishNode, FindOrCreate, SetHero, SeedSiteSettings, ses cles GUID d'element et ses MediaSources).

Ajouts 2026-07-17 : Number(int) (Umbraco.Integer) et ContentPick(Guid) (Umbraco.ContentPicker, valeur = UDI document construite via Udi.Create plutot qu'un "umb://document/{key:N}" formate a la main). ContentPick sert childListing.source — c'est ce qui fait d'une liste une vue des enfants d'une autre page au lieu de contenu duplique (l'accueil de chevaliers pointe ainsi sur le noeud Activites).

Generateur de seed (tools/seed-gen/)

Le seeder de chevaliers n'est pas ecrit a la main : DevSeedChevaliersController.cs est genere. Ne pas l'editer — modifier le generateur et relancer.

FichierRole
chevaliers-bands.pyScrape le live par bande, dans l'ordre du DOM -> data/bands.json. Regle porteuse structurelle : une .row contenant une .row-img est un mediaText (le theme ecrit cette meme ligne de 4 facons ; matcher la classe perd des lignes). Ecrit aussi trois cles _-prefixees (metadonnees, pas des pages — tout compteur doit les filtrer sur le prefixe, pas nommer une cle) : _site (nav/pied de page), _listingHeroes + _listingCtas (banniere de titre et banniere de renvoi des pages de listing, dont les lignes sont pilotees par requete et qui n'etaient donc dans aucune liste), _activityDetails (bandeau « En bref… » et formulaire des pages activite).
chevaliers-gen.pydata/{bands,content,events}.json -> le controller C#. Une bande = un bloc.
data/Le scrape, dans le repo (avant : un scratchpad de session, efface -> generateur injouable). data/html/ = les pages live en cache ; --refresh re-telecharge.

Ce que le generateur signale a chaque run — jamais en silence : DROPPED (URL non-image : le live n'a pas d'image la), NOT PORTED (bandes mesurees a zero pixel, cf. domain/assets-substitues.md §6), HOTLINKED (icones en ligne pointant encore sur chevaliers.ch).

Lieux d'accueil gilliard (gilliard-venues.mjs -> GilliardVenueData.cs)

Meme principe, cote gilliard, pour les dix pages /fr/<slug>/ de lieux d'accueil :

FichierRole
gilliard-venues.mjsScrape les 10 pages live -> data/gilliard-venues.json (hero, intro, lignes, « En bref… », formulaire, embed, CTA).
gilliard-venues-gen.mjsdata/gilliard-venues.json -> src/Web/Seed/GilliardVenueData.cs (genere : ne jamais l'editer a la main).

DevSeedPagesController.SeedVenue() ne fait plus que traduire ces donnees en blocs. Le media detail est concatene a MediaSources ([.. MediaSources, .. GilliardVenueData.Media]), donc il reste un seul passage d'import et un seul dictionnaire media.

Ce que la mesure du 2026-07-23 a corrige (l'ancienne version prenait une ligne et un formulaire en parametres fixes, faux sur 9 lieux sur 10) :

  • le live sert les lieux sous deux gabarits.page-team > .row.team-info-row et .single-experience > .row.single-experience-row. Scoper la requete de lignes au premier rendait salle-de-seminaire vide ; la regle fiable reste structurelle (.row contenant directement .row-img), jamais la classe ;
  • le nombre de lignes va de 1 a 5 ; pop-up-bar-le-qg n'a pas de formulaire ; salle-de-seminaire n'a pas de CTA et ouvre par une bande « En bref… » (icone + une ligne de texte = nos cartes info) ; clos-du-mont et clos-de-la-cochetta portent un embed Google Maps (873x450 / 873x580) entre les lignes et le formulaire — porte par le bloc partage embed en provider html, donc sans changement de schema et toujours sous consentement ;
  • aucune des 20 lignes live ne porte d'embleme : le .logo du live contient un GIF 1x1 qui n'est jamais remplace (data-src vide). L'ancien seeder posait nv-emblem partout.

⚠️ Piege de sonde : forcer img.src = img.dataset.src casse ces images (data-src vide -> src=""), le navigateur affiche alors le texte alt et la ligne mesure 88px au lieu de 24. Ne forcer b-lazy que si data-src est non vide, sinon la mesure ment.

Pieges du generateur (tous rencontres pour de vrai) :

  • La table MediaSources est emise AVANT la section des pages. Tout media() appele plus tard n'y arrive jamais. D'ou le pre-calcul (PAGE_BLOCKS/HOME_BLOCKS) avant l'emission. Le symptome etait muet : Img() retombait sur Text("") — une valeur TextBox vide sur une propriete MediaPicker3 — et l'image disparaissait sans erreur. Img() leve desormais.
  • List(params Block[]) : avec un seul element, C# cible Block[] pour le new() et echoue (CS8752). Emettre new Block(...) explicitement.
  • Ne jamais taper une URL de media a la main (une banniere devinee depuis un dump tronque = un 404 qui avorte tout l'import). Les images du hero sont lues depuis bands.json.
  • Les pages de detail d'activite du live sont sur /fr/activite/<slug>/ — au SINGULIER./fr/activites/<slug>/ est un 404, et comparer une page locale a ce 404 fabrique un « le live n'a pas de hero ici » qui n'existe pas. fetch(..., path_prefix="activite").
  • Le balisage du live n'est pas toujours bien forme : /activites/ porte sa banniere en data-src="…iStock-1215776606-2600x1095.jpg'" — apostrophe a l'interieur de l'attribut. La regex d'extension echouait en fin de chaine et l'image partait a la poubelle en silence. clean_url() retire les guillemets parasites avant toute comparaison.
  • ⚠️ contains(@class,"title") matche aussi subTITLE. La banniere de renvoi du live est h3.subtitle au-dessus de h1/h2.title : sans correspondance par jeton (contains(concat(" ", normalize-space(@class), " "), " title ")), le titre de chaque banniere devenait son propre sur-titre.
  • notFoundPage ne compose que pageSettings : sa propriete est notFoundTitle, pas heroTitle.
  • Ne jamais re-sauver un noeud deja publie dans le meme run : l'instance IContent est perimee -> InvalidOperationException: Cannot save a non-current version. Le chrome re-charge la racine (ContentService.GetById(ctx.RootId)) parce que la section Accueil l'a deja publiee.
  • Ordre : les textPage legales sont semees avant le chrome, qui pointe sur leurs noeuds.

UrlMode.Default resout l'URL par rapport au site de la REQUETE COURANTE. Or /seed-chevaliers resout sa racine par doctype et peut donc etre appele depuis n'importe quel hote : en l'appelant depuis gilliard.localtest.me, tous les noeuds chevaliers passaient pour cross-site et Default emettait des URLs absolueshttps://chevaliers.localtest.me:44360/activites/ grave dans le contenu. Le contenu seme dependait donc de l'hote depuis lequel on lancait le seeder, et un nom d'hote de dev serait parti en prod. Relative est independant de l'hote et correct ici : ces liens sont toujours same-site (tout ce qui est hors perimetre est seme en URL absolue explicite vers le live). Corrige 2026-07-17 ; les deux seeders re-joues, gilliard non regresse.

Extensions de contenu (src/Web/Extensions/)

  • PageExtensions : GetHeadTitle, GetMetaTitle, GetMetaDescription, GetOgTitle, GetOgDescription (resolution par priorite via l'interface generee IPageSettings).
  • DateTimeExtensions (ex. FormatAsW3CDateTime pour le sitemap), MediaWithCropsExtensions, PublishedContentExtensions (ex. GetAbsoluteUrl, GetTemplateAlias, HideFromSiteMap), UmbracoContextExtensions (ex. GetCultureFromDomains).

Contributors

No contributors

Changelog

No recent changes