Module - Clients (skins) et multi-tenant
Le modele multi-tenant : un socle de code (base + shop), 30+ clients materialises par des "skins".
Anatomie d'un client (skin)
Pour un client <skin> (nom court, ex. davidchoco, ieds, cmveo) :
| Element | Emplacement |
|---|---|
| Code custom | src/Nanoxi.Cms.Custom/<Client>/ (sous-projets Data, Mvc, et Shop si e-commerce) |
| Solution client | src/Nanoxi.Cms.Custom/StarterKitv3_<skin>.sln |
| Sources design (LESS) | design/v2/<skin>/ (Gruntfile.js + Skins/) |
| Config production | prodconfig/<skin>/ (transforms Web.config, licence) |
| Metadonnees | entree dans doc/StarterKitv3_InfosTechniques.json |
Le build cible UN skin via --skin=. Build-Customer compile StarterKitv3_<skin>.sln si elle existe ; Build-All-Customers les compile toutes.
Configuration declarative (InfosTechniques.json)
doc/StarterKitv3_InfosTechniques.json est un tableau d'objets, un par client. Chaque entree declare :
name,skinName,group,coordinatessite.inProduction,site.languages(ex.["fr","de","en"]),site.url(test/production)site.connection.dataBase(serverName,saPassword,customerPassword)site.configuration:widgets,modules(dont ShopisActif),socialShares,packs(MediaProtect, UmbracoForms)
Le build valide ce JSON contre StarterKitv3_InfosTechniques_Schema.json au demarrage et echoue si invalide. La lib CustomerClientConfig (de Nanoxi.Cms.Builder.Library) lit ce fichier ; IsShopEnabled() derive du module Shop.
Liste des clients (extrait)
Plus de 30 solutions existent dans src/Nanoxi.Cms.Custom/StarterKitv3_*.sln, notamment : accmpd, actiondiabete, apol, arbora, bagnomio, biofruits, blandes, bourgeoisie, boutgilles, cmveo, cotemin, cpob, creactif, cvdp, davidchoco, differens, drgabs, fctvs, frommol, fsh, gentremont, geo4me, ieds, leflaur, matplus, ovs, pforet, piscinesp, psymed, relaisdor, secoss, smartcity, starterkitv3 (reference), timmo, ucova, valdeb, valdem, vchalet.
Specificites client notables
IEDS (e-commerce + extranet)
- Extranet "Prevention et controle d'infection" : synchro avec IchConnector (lib
IchData, .NET 4, referencee dansNanoxi.Cms.Custom.Ieds.Data). Execution 09h00 / 12h00 / 18h00.- Point d'entree :
ContentManagerController(Start / StartWorkOn / CreateRecord / CreateFile / EndAndClean). - Table petaPoco :
nanoxi_IchMatchingGlobalMedia(etat de synchro, version, langue FR/DE). - Index Examine custom
IchMedia: DB et media doivent matcher ; en cas d'incoherence, stopper le pool, viderwebsite/App_Data/TEMP/ExamineIndexes/IchMedia/Index, redemarrer, puis reconstruire via/Umbraco/Api/ContentManager/RecreateIchMediaIndex/.
- Point d'entree :
- Shop synchronise avec Opale (08h00) : voir
shop-ecommerce.md. Dossier d'echangeC:\ieds_shared. Integration nopCommerce egalement. - Un redemarrage de pool + website resout la plupart des erreurs de synchro.
CMVEO (calculateur de primes d'assurance)
- Donnees de primes par annee :
Nanoxi.Cms.Custom.Cmveo.PremiumData.Concrete.Data._20xx. Pour une nouvelle annee, dupliquer le dossier et adapterEnvironmentalTaxManager.cs(methodeEnvironmentalTax(DateTime)) +nanoxiSettings.config(champreleaseNewPrimesDate). - Formulaire multi-etapes (HomeBanner -> PageAddPersonn -> PageAddPersonnValidate) pilote par le controller
CalculatorApi; validations JS dansScripts/Cmveo/et serveur dansPersonConfigParameters.ValidatePersonData(). - Le systeme avait ete concu hors Docker : pour une mise a jour ponctuelle, soit MAJ complete du site, soit remplacer la DLL +
nanoxiSettings.configdans le conteneur et redemarrer.
matplus (multi-sous-sites dans une seule instance)
Un seul site matplus heberge plusieurs marques, chacune materialisee par un noeud racine de premier niveau (enfant direct de -1) portant une propriete skin : Materiaux Plus (default), Proz (proz), Promat (promat), Gruber (gruber) et Rhone Color (rhone, ajoute en avril 2025 comme sous-site de Promat).
- Resolution du skin :
DomainUtil.GetSkin(content)lit la proprieteskinsurpath[1], c'est-a-dire le noeud racine de premier niveau du contenu. Consequence : si on passe un contenu de la section My Data (RootData, egalement de premier niveau), le skin retourne "" puis la valeur de repli"default". C'est pour cela queAdvisorController.Showrend toujours~/Views/Advisor/default/Show.cshtml, quel que soit le sous-site — les dossiersgruber/,promat/,proz/de ce meme repertoire ne servent qu'aIndex(appele depuisPageTeam.cshtml, avec une vraie page en argument). - Liste des sous-sites cote code : appSetting
matplusSubsites(prodconfig/matplus/config/nanoxiSettings*.config), formatdomaine,AffiliateNodeId|domaine,AffiliateNodeId|..., chargee au demarrage dansMatplusDefs.SubsitesparCustomApplicationEventHandler. Tout nouveau sous-site doit y etre ajoute :AdvisorController.IndexetInspirationControllerfontSubsites.Where(...).FirstOrDefault()puis dereferencentcurrentDomain.AffiliateNodeIdsans garde -> NullReferenceException sur un domaine inconnu. - Portee de
UmbracoUtil.GetNodesByTemplateName(currentPageId, ...): la procedure stockeenanoxi_GetNodeIdByTemplateNamefiltre surpath LIKE '-1,<noeud de premier niveau du contenu courant>,%'. Dans une instance multi-sous-sites, la recherche est donc limitee au sous-site courant : un sous-site sans page portant le template demande renvoie une liste vide, etFirstOrDefault()surList<int>rend0->TypedContent(0)rendnull. Toujours testerid > 0et leIPublishedContentavant de dereferencer (cas rencontre sur Rhone Color : aucune pagePageTeam, le bloc conseiller dePageProduct/PageServicefaisait tomber toutes les pages produits en 500). - La procedure filtre sur
cmsDocument.newest = 1, pas surpublished = 1: une page enregistree mais non publiee renvoie bien un id, dontTypedContentrendnull. Meme garde necessaire. - Filiale non renseignee :
AdvisoretInspiration(matplus/Data/DocType/) remplissent leur proprieteFilialedans untry/catchqui se contente de logger. Si le pickerdoctype*Filialeest vide, l'exception est avalee et la valeur reste celle deInitValues()—string.EmptypourAdvisor,nullpourInspiration. CoteInspiration, toutFiliale.Contains(...)sans garde part donc en NullReferenceException, et le contenu concerne disparait silencieusement de tous les sous-sites. - Checklist nouveau sous-site matplus : noeud racine + propriete
skin; entree dansmatplusSubsites; dossiers de vuesViews/<Zone>/<skin>/(Footer, Inspiration, Language, Menu, Shared, Widget — etAdvisor/si une page equipe existe) declares en<Content Include>dansNanoxi.Cms.Custom.Matplus.Mvc.csproj; skin designdesign/v2/matplus/Skins/<Skin>/; pages speciales attendues par les widgets (PageTeam,PageInspirationGrid).
accmpd (Linfo - magazine d'articles geolocalises)
Deux sous-sites, deux skins : le client accmpd ("Accm plateforme digitale") sert
www.icogne.chavec le skindefaultetwww.l-info.chavec le skinlinfo.Homebanner icogne (skin default) : les images ne viennent PAS du backoffice Umbraco. Ce sont des assets statiques du skin (
design/v2/accmpd/Skins/Default/Webparts/HomeBanner/Img/, bg-1..bg-4 en .jpg/.webp). La vuedefault/HomeBanner.cshtmlposedata-maximg="4"sans balise image ;wp-homebanner.jstire undata-numaleatoire 1..4 et le LESS (wp-homebanner.less, mixin.crops_image) mappe chaquedata-numsur unbackground-imagea 3 couches : motif chevronsFoundation/themes/pattern-sapin-home.svg(etire en100% auto) + calque degrade vert/bleulinear-gradient(rgba(108,200,44,.4), rgba(0,156,205,.4))+ photo, recadree a la volee par ImageProcessor (?width=..&height=..&crop=auto) selon les breakpoints. Historique : la refonte de juillet 2025 (commits522489578..106201c67) avait fige la banniere sur une seule imagebg-8.jpgsans degrade ni motif ; la rotation bg-1..4 + degrade + motif a ete retablie ensuite. Toute modif passe par le LESS puisgrunt lessdansdesign/v2/accmpd/(grunt 0.4.5, fonctionne vianpx grunt-cli), jamais par le CSS compile. ("Linfo") : les articles portent des coordonnees Terratype (WGS84) dans la proprietearticleGeolocations(Archetype), parsees dansCustomerArticle(Data/Article/Model/ArticleAccmpd.cs, champLatLng). La vue grille et la carte partagent les memes filtres (rubrique/type/tag/annee) ; la carte est alimentee en AJAX parWpArticleLinfoController.GetMapData, le JS d'affichage estdesign/v2/accmpd/.../Blogmap/Scripts/wp-blogmap.js(Google Maps + markerclusterer).Cache :
LinfoCacheHelpermet en cache le viewmodel par combinaison de filtres (HttpRuntime.Cache, 1 jour). Premier appel d'une combinaison = calcul a froid (ArticleManager.Blog.Work), appels suivants = cache. Pour invalider :LinfoCacheHelper.RemoveAllArticlesFromCache()(ou redemarrer le pool).Piege coordonnees / culture : ne jamais serialiser/parser une coordonnee via une chaine
"lat,lng". La virgule sert de separateur de champ mais est aussi le separateur decimal sous certaines cultures de thread (le chemin a froid laisse parfois le thread sous une culture a virgule,Web.configne fixant aucuneculture=). Resultat historique : la latitude s'ecrasait sur sa partie entiere et les points s'alignaient en ligne horizontale, uniquement au premier affichage d'un filtre (a froid).GetMapDatagarde desormais lesdoublede bout en bout (groupement sur le couple lat/lng, pas de round-trip string). Tout nouveau code carto doit faire de meme (ou formater/parser enInvariantCulture).Newsletter en double opt-in : le skin accmpd sert deux sous-sites avec deux listes Infomaniak distinctes (Icogne, Linfo). L'inscription ne passe plus jamais en direct chez Infomaniak :
AccmpdNewsletterController.RegisterNewsletterInfomaniakappelle la Nanoxi Newsletter API (newsletterOptInApiUrl, routeInfomaniak/DoubleOptIn), qui stocke la demande, envoie le mail de confirmation et ne cree l'abonne qu'apres ouverture du lien. Code :Mvc/Services/AccmpdNewsletterOptInService.cs.Liste cible resolue cote serveur : le sous-site est deduit du host de la requete (
icogneNewsletterHosts/linfoNewsletterHosts), plus d'un champ cache du formulaire. Un champSkinposte par le client laissait auparavant l'appelant choisir sa liste.Piege token antiforgery / footer en cache : le formulaire newsletter vit dans
Views/Footer/default/FooterBanner.cshtmletViews/Footer/linfo/FooterAffiliate.cshtml, rendus viaCachedPartial2(starterCachedSeconds= 86400 en production, 0 en dev). Un@Html.AntiForgeryToken()place dans le formulaire serait mis en cache 24h et partage par tous les visiteurs, donc invalide pour chacun d'eux. Le token est donc rendu dansFooterAction.cshtml, hors du bloc cache, dans un conteneur dedie#WG-newsletter-antiforgery.wg-newsletter.jsdoit scoper son selecteur sur ce conteneur : d'autres tokens vivent dans la page et certains sont rendus dans un bloc cache (widget contact ->Widget/WidgetContact.cshtmlviaCachedPartial2). Un selecteur global ($('input[name="__RequestVerificationToken"]').first()) prend le premier du DOM, donc un token en cache et invalide pour le visiteur courant, et la newsletter tombe en 500 sur toute page portant un widget contact. Le symptome est invisible en dev (starterCachedSeconds= 0). Meme precaution pour tout futur champ par-visiteur dans un partial cache.Garde-fous anti-abus :
[ValidateAntiForgeryToken],EmailUtil.IsValidEmail, honeypot (champwebsite), et limitation de debit par IP et par adresse (AccmpdNewsletterThrottle, clesnewsletterOptInMaxPerIpPerHour/newsletterOptInMaxPerEmailPerDay). Le double opt-in seul ne protege pas : sans limite, un robot transforme l'endpoint en relais d'envoi de mails de confirmation vers des adresses tierces. Compteurs en memoire, donc par processus.
Detail complet des specificites client : ReadmeCustomer.md (source de verite a la racine du repo).

