Skip to content

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) :

ElementEmplacement
Code customsrc/Nanoxi.Cms.Custom/<Client>/ (sous-projets Data, Mvc, et Shop si e-commerce)
Solution clientsrc/Nanoxi.Cms.Custom/StarterKitv3_<skin>.sln
Sources design (LESS)design/v2/<skin>/ (Gruntfile.js + Skins/)
Config productionprodconfig/<skin>/ (transforms Web.config, licence)
Metadonneesentree 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, coordinates
  • site.inProduction, site.languages (ex. ["fr","de","en"]), site.url (test/production)
  • site.connection.dataBase (serverName, saPassword, customerPassword)
  • site.configuration : widgets, modules (dont Shop isActif), 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 dans Nanoxi.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, vider website/App_Data/TEMP/ExamineIndexes/IchMedia/Index, redemarrer, puis reconstruire via /Umbraco/Api/ContentManager/RecreateIchMediaIndex/.
  • Shop synchronise avec Opale (08h00) : voir shop-ecommerce.md. Dossier d'echange C:\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 adapter EnvironmentalTaxManager.cs (methode EnvironmentalTax(DateTime)) + nanoxiSettings.config (champ releaseNewPrimesDate).
  • Formulaire multi-etapes (HomeBanner -> PageAddPersonn -> PageAddPersonnValidate) pilote par le controller CalculatorApi ; validations JS dans Scripts/Cmveo/ et serveur dans PersonConfigParameters.ValidatePersonData().
  • Le systeme avait ete concu hors Docker : pour une mise a jour ponctuelle, soit MAJ complete du site, soit remplacer la DLL + nanoxiSettings.config dans 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 propriete skin sur path[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 que AdvisorController.Show rend toujours ~/Views/Advisor/default/Show.cshtml, quel que soit le sous-site — les dossiers gruber/, promat/, proz/ de ce meme repertoire ne servent qu'a Index (appele depuis PageTeam.cshtml, avec une vraie page en argument).
  • Liste des sous-sites cote code : appSetting matplusSubsites (prodconfig/matplus/config/nanoxiSettings*.config), format domaine,AffiliateNodeId|domaine,AffiliateNodeId|..., chargee au demarrage dans MatplusDefs.Subsites par CustomApplicationEventHandler. Tout nouveau sous-site doit y etre ajoute : AdvisorController.Index et InspirationController font Subsites.Where(...).FirstOrDefault() puis dereferencent currentDomain.AffiliateNodeId sans garde -> NullReferenceException sur un domaine inconnu.
  • Portee de UmbracoUtil.GetNodesByTemplateName(currentPageId, ...) : la procedure stockee nanoxi_GetNodeIdByTemplateName filtre sur path 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, et FirstOrDefault() sur List<int> rend 0 -> TypedContent(0) rend null. Toujours tester id > 0 et le IPublishedContent avant de dereferencer (cas rencontre sur Rhone Color : aucune page PageTeam, le bloc conseiller de PageProduct/PageService faisait tomber toutes les pages produits en 500).
  • La procedure filtre sur cmsDocument.newest = 1, pas sur published = 1 : une page enregistree mais non publiee renvoie bien un id, dont TypedContent rend null. Meme garde necessaire.
  • Filiale non renseignee : Advisor et Inspiration (matplus/Data/DocType/) remplissent leur propriete Filiale dans un try/catch qui se contente de logger. Si le picker doctype*Filiale est vide, l'exception est avalee et la valeur reste celle de InitValues()string.Empty pour Advisor, null pour Inspiration. Cote Inspiration, tout Filiale.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 dans matplusSubsites ; dossiers de vues Views/<Zone>/<skin>/ (Footer, Inspiration, Language, Menu, Shared, Widget — et Advisor/ si une page equipe existe) declares en <Content Include> dans Nanoxi.Cms.Custom.Matplus.Mvc.csproj ; skin design design/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.ch avec le skin default et www.l-info.ch avec le skin linfo.

  • 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 vue default/HomeBanner.cshtml pose data-maximg="4" sans balise image ; wp-homebanner.js tire un data-num aleatoire 1..4 et le LESS (wp-homebanner.less, mixin .crops_image) mappe chaque data-num sur un background-image a 3 couches : motif chevrons Foundation/themes/pattern-sapin-home.svg (etire en 100% auto) + calque degrade vert/bleu linear-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 (commits 522489578..106201c67) avait fige la banniere sur une seule image bg-8.jpg sans degrade ni motif ; la rotation bg-1..4 + degrade + motif a ete retablie ensuite. Toute modif passe par le LESS puis grunt less dans design/v2/accmpd/ (grunt 0.4.5, fonctionne via npx grunt-cli), jamais par le CSS compile. ("Linfo") : les articles portent des coordonnees Terratype (WGS84) dans la propriete articleGeolocations (Archetype), parsees dans CustomerArticle (Data/Article/Model/ArticleAccmpd.cs, champ LatLng). La vue grille et la carte partagent les memes filtres (rubrique/type/tag/annee) ; la carte est alimentee en AJAX par WpArticleLinfoController.GetMapData, le JS d'affichage est design/v2/accmpd/.../Blogmap/Scripts/wp-blogmap.js (Google Maps + markerclusterer).

  • Cache : LinfoCacheHelper met 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.config ne fixant aucune culture=). 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). GetMapData garde desormais les double de 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 en InvariantCulture).

  • 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.RegisterNewsletterInfomaniak appelle la Nanoxi Newsletter API (newsletterOptInApiUrl, route Infomaniak/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 champ Skin poste 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.cshtml et Views/Footer/linfo/FooterAffiliate.cshtml, rendus via CachedPartial2 (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 dans FooterAction.cshtml, hors du bloc cache, dans un conteneur dedie #WG-newsletter-antiforgery. wg-newsletter.js doit 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.cshtml via CachedPartial2). 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 (champ website), et limitation de debit par IP et par adresse (AccmpdNewsletterThrottle, cles newsletterOptInMaxPerIpPerHour / 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).

Contributors

No contributors

Changelog

No recent changes