Une DSO dentaire avec 50 cabinets a le même problème d'architecture de site web qu'une chaîne de salles de sport avec 200 sites, un groupe hôtelier avec 30 propriétés et un réseau d'églises avec 15 campus. Ils ont tous besoin de : un contrôle centralisé de la marque, du contenu localisé par site, un seul tableau de bord d'administration, des pages SEO par site, et un déploiement qui met à jour tout simultanément sans rien casser. L'architecture est identique. Le contenu est différent.

J'ai construit ce pattern pour des groupes dentaires, des franchises de fitness, des réseaux vétérinaires et des chaînes de restaurants. À chaque fois, je commence par le même schéma de base de données, la même structure de routes Next.js et le même contrôle d'accès basé sur les rôles. Ce qui change, ce sont les données d'amorçage et les étiquettes de composants. « Services » devient « Cours » dans une salle de sport ou « Articles de menu » dans un restaurant. « Personnel » devient « Dentistes » ou « Entraîneurs » ou « Vétérinaires ». La plomberie en dessous ? Identique.

Cet article présente le pattern d'architecture multi-sites universel une fois, puis montre comment il s'adapte à cinq industries complètement différentes. Si vous gérez une entreprise multi-sites — ou si vous êtes un développeur construisant pour l'une — ceci est le blueprint.

Table des matières

Multi-Site Architecture for DSOs, Vet Chains, Gyms & Franchises

Le problème fondamental auquel chaque entreprise multi-sites est confrontée

Soyons honnêtes sur ce qui se produit généralement. Une franchise ou une entreprise multi-sites commence par un seul site web. Puis ils ouvrent un deuxième site. Quelqu'un met en place une deuxième installation WordPress. Quand il y a 15 sites, vous avez 15 sites WordPress distincts, 15 thèmes différents (certains ont trois versions de retard), 15 ensembles différents de plugins, et zéro contrôle centralisé.

La directrice du marketing veut mettre à jour le CTA principal de la marque sur tous les sites. C'est 15 connexions, 15 éditions et une prière que personne n'ait cassé son template. L'équipe SEO veut voir quels sites publient du contenu de blog et lesquels sont restés silencieux pendant six mois. Il n'y a pas de tableau de bord pour cela — juste une feuille de calcul que quelqu'un a oublié de mettre à jour en mars.

C'est le même problème, que vous soyez une organisation de support dentaire (DSO) gérant 50 cabinets ou un groupe de restaurants avec 200 sites. Les symptômes sont identiques :

  • Dérive de la marque. Les sites s'éloignent de la marque parce que personne n'applique la cohérence.
  • Fragmentation SEO. Pas de pages SEO locales structurées, pas de cohérence du balisage de schéma, pas de sitemap centralisé.
  • Chaos administratif. Chaque site gère son propre site (mal), ou le siège social gère tout (lentement).
  • Risque de déploiement. Mettre à jour le site d'un site ne devrait pas pouvoir faire tomber celui d'un autre.

La solution n'est pas un meilleur thème CMS. C'est une architecture complètement différente.

Le schéma de base de données universel

Tout commence par une table locations. C'est l'ancre de l'ensemble du système. J'utilise Supabase comme couche de base de données et d'authentification parce qu'il vous donne Postgres, Row-Level Security, des abonnements en temps réel et un niveau gratuit généreux — mais le schéma fonctionne avec n'importe quelle base de données relationnelle.

Voici le schéma fondamental :

-- La table d'ancrage. Chaque élément de contenu spécifique à un site
-- fait référence à ceci via location_id.
CREATE TABLE locations (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  address TEXT NOT NULL,
  city TEXT NOT NULL,
  state TEXT NOT NULL,
  zip TEXT NOT NULL,
  lat DECIMAL(10, 8),
  lng DECIMAL(11, 8),
  phone TEXT,
  email TEXT,
  hours JSONB DEFAULT '{}',
  photos TEXT[] DEFAULT '{}',
  description TEXT,
  metadata JSONB DEFAULT '{}',
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- Les tables de contenu suivent le MÊME pattern :
-- location_id est NULLABLE.
-- NULL = partagé sur tous les sites
-- Une valeur = spécifique à ce site

CREATE TABLE services (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  description TEXT,
  price_range TEXT,
  duration TEXT,
  category TEXT,
  sort_order INT DEFAULT 0,
  is_active BOOLEAN DEFAULT true,
  metadata JSONB DEFAULT '{}'
);

CREATE TABLE staff (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  title TEXT,
  photo TEXT,
  bio TEXT,
  credentials TEXT[],
  specialties TEXT[],
  sort_order INT DEFAULT 0,
  is_active BOOLEAN DEFAULT true
);

CREATE TABLE blog_posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  content TEXT,
  excerpt TEXT,
  author_id UUID REFERENCES staff(id),
  published_at TIMESTAMPTZ,
  is_published BOOLEAN DEFAULT false,
  tags TEXT[] DEFAULT '{}',
  metadata JSONB DEFAULT '{}'
);

CREATE TABLE testimonials (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  author_name TEXT NOT NULL,
  rating INT CHECK (rating >= 1 AND rating <= 5),
  content TEXT,
  is_approved BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE events (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  description TEXT,
  event_date TIMESTAMPTZ,
  end_date TIMESTAMPTZ,
  is_active BOOLEAN DEFAULT true
);

Le pattern de location_id nullable est l'insight clé. Quand un article de blog a location_id = NULL, c'est un article au niveau du réseau (« 5 conseils pour des dents saines » partagés sur les 50 cabinets dentaires). Quand location_id a une valeur, c'est spécifique à ce site (« Le Dr Smith rejoint notre cabinet d'Austin »). Même table, mêmes patterns de requête, mais le contenu peut être partagé ou localisé avec une seule colonne.

La colonne metadata JSONB est l'endroit où vivent les champs spécifiques à l'industrie. Un site dentaire pourrait stocker {"insurance_accepted": ["Delta Dental", "Cigna"], "parking_info": "Lot gratuit derrière le bâtiment"}. Une salle de sport stocke {"equipment": ["squat racks", "rowing machines"], "peak_hours": "5-7 PM weekdays"}. Aucune migration de schéma nécessaire — juste différentes formes JSON.

Architecture des routes Next.js

Le App Router Next.js correspond clairement à ce modèle de données. Voici la structure de routes qui fonctionne pour chaque industrie :

app/
├── page.tsx                          # Page d'accueil
├── locations/
│   ├── page.tsx                      # Chercheur de site (carte + recherche géo)
│   └── [slug]/
│       ├── page.tsx                  # Page de détail du site
│       ├── staff/page.tsx            # Listing du personnel pour le site
│       └── services/page.tsx         # Services pour le site
├── services/
│   └── [service]/page.tsx            # Description de service partagée
├── blog/
│   ├── page.tsx                      # Tous les articles de blog
│   └── [post]/page.tsx               # Article de blog individuel
├── about/page.tsx
└── contact/page.tsx

La page de détail du site (/locations/[slug]) est là que la magie opère. Un seul appel generateStaticParams interroge chaque site actif et les pré-rend tous au moment de la construction :

// app/locations/[slug]/page.tsx
import { createClient } from '@/lib/supabase/server'

export async function generateStaticParams() {
  const supabase = createClient()
  const { data: locations } = await supabase
    .from('locations')
    .select('slug')
    .eq('is_active', true)

  return locations?.map((loc) => ({ slug: loc.slug })) ?? []
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const supabase = createClient()
  const { data: location } = await supabase
    .from('locations')
    .select('*')
    .eq('slug', params.slug)
    .single()

  if (!location) return {}

  return {
    title: `${location.name} | ${location.city}, ${location.state}`,
    description: location.description,
    openGraph: {
      title: `${location.name} - ${location.city}`,
      images: location.photos?.[0] ? [location.photos[0]] : [],
    },
  }
}

export default async function LocationPage({ params }: { params: { slug: string } }) {
  const supabase = createClient()
  
  const [{ data: location }, { data: staff }, { data: services }, { data: testimonials }] = 
    await Promise.all([
      supabase.from('locations').select('*').eq('slug', params.slug).single(),
      supabase.from('staff').select('*').eq('location_id', params.slug), // simplified
      supabase.from('services').select('*').or(`location_id.is.null,location_id.eq.${locationId}`),
      supabase.from('testimonials').select('*').eq('is_approved', true),
    ])

  // Rendre la page du site avec toutes les données
  // C'est la même structure de composants quel que soit l'industrie
}

La requête de services utilise ce filtre or — récupérer les services où location_id est null (services partagés) OU correspond au site actuel. Cela signifie qu'une DSO dentaire peut définir « Nettoyage des dents » une fois pour tous les sites, puis ajouter « Invisalign » uniquement pour les sites qui l'offrent. Aucune duplication.

Pour la page de chercheur de sites, je stocke les coordonnées lat/lng et j'utilise l'extension PostGIS de Supabase pour les requêtes géographiques :

-- Trouver les sites à moins de 40 km des coordonnées de l'utilisateur
SELECT *, 
  (point(lng, lat) <@> point($1, $2)) * 1.60934 AS distance_miles
FROM locations
WHERE is_active = true
ORDER BY point(lng, lat) <@> point($1, $2)
LIMIT 20;

Multi-Site Architecture for DSOs, Vet Chains, Gyms & Franchises - architecture

Row-Level Security et le tableau de bord d'administration

C'est là que l'architecture paie vraiment. Les politiques RLS Supabase vous permettent de définir l'accès aux données au niveau de la base de données — pas dans le code de votre application.

-- Les responsables de site ne peuvent voir que les données de leur propre site
CREATE POLICY "Location managers see own data" ON services
  FOR ALL
  USING (
    location_id IN (
      SELECT location_id FROM user_locations
      WHERE user_id = auth.uid()
    )
    OR
    EXISTS (
      SELECT 1 FROM user_roles
      WHERE user_id = auth.uid() AND role = 'network_admin'
    )
  );

Les administrateurs du réseau voient tout. Les responsables de site ne voient que leur site. Cela s'applique à chaque table — services, personnel, articles de blog, témoignages, événements. Un pattern de politique, appliqué de manière cohérente.

Le tableau de bord d'administration affiche les métriques au niveau du réseau :

  • Fraîcheur du contenu : Quels sites n'ont pas mis à jour leur blog depuis 30+ jours ?
  • Trafic par site : Données de Google Search Console agrégées par slug de site
  • Prospects par site : Soumissions de formulaires et demandes de réservation par site
  • Conformité de marque : Tous les sites utilisent-ils le logo approuvé, les couleurs et le texte CTA ?

Variation industrielle 1 : DSO dentaires

Un site web de DSO doit avoir l'impression d'être une marque dentaire unifiée tout en permettant à chaque cabinet de mettre en avant ses fournisseurs et spécialités uniques.

Services correspondent à des procédures dentaires : nettoyages, détartrage, couronnes, implants, Invisalign, soins dentaires d'urgence. Certains sont universels (chaque site fait des nettoyages), d'autres sont spécifiques au site (seulement trois sites proposent la dentisterie sous sédation).

Personnel sont des dentistes, hygiénistes et responsables de bureau. Chacun obtient un profil avec des accréditations (DDS, DMD), spécialités, éducation et une photo professionnelle. Les parents choisissant un dentiste pédiatrique veulent voir qui traitera leur enfant.

CTA est « Prendre un rendez-vous ». Cela se connecte à Calendly, NexHealth ou un système de réservation personnalisé. Le widget de réservation pré-sélectionne le site en fonction duquel le site le utilisateur provient.

Cibles SEO locales : « dentiste à [ville] », « [procédure] à [ville] », « dentiste urgence [ville] [état] ». Chaque page de site obtient le balisage de données structurées pour les schémas Dentist et LocalBusiness.

Metadata JSONB stocke : plans d'assurance acceptés, informations de stationnement, caractéristiques d'accessibilité, langues parlées, s'ils acceptent les nouveaux patients.

Variation industrielle 2 : Chaînes de salles de sport et fitness

Les chaînes de salles de sport échangent « services » contre « cours » — mais le modèle de données est le même. Un cours de yoga au site A et un cours HIIT au site B ne sont que des lignes dans la table services avec différentes valeurs location_id.

Services sont les types de cours avec des données d'horaire. Les métadonnées stockent l'horaire hebdomadaire en JSON, l'affectation d'instructeur, les limites de capacité et s'il y a des accès libres.

Personnel sont des entraîneurs et instructeurs avec des certifications (NASM, ACE, CrossFit L2), spécialités et disponibilité pour les réservations d'entraînement personnel.

CTA est « Rejoindre maintenant » — un paiement d'abonnement Stripe qui gère les niveaux d'adhésion et l'accès à plusieurs sites. Un membre qui s'inscrit au site du centre-ville devrait pouvoir s'enregistrer au site suburbain aussi.

Cibles SEO locales : « salle de sport près de moi », « cours de fitness [ville] », « cours [type de classe] [ville] », « entraîneur personnel [ville] ».

Metadata JSONB stocke : liste d'équipement, horaire des cours, heures de pointe, équipements (sauna, piscine, garderie), disponibilité du stationnement gratuit.

Variation industrielle 3 : Groupes hôteliers

Les groupes hôteliers de charme et les chaînes hôtelières indépendantes bénéficient énormément de ce pattern — particulièrement parce qu'il permet les réservations directes qui contournent les frais des OTA (généralement 15-25 % par réservation sur Booking.com ou Expedia).

Services deviennent les types de chambres : Chambre Standard, Suite King, Penthouse. Chacun obtient des photos, des listes d'équipements, la superficie et la tarification de base. La tarification spécifique au site vit dans les métadonnées ou une table de tarifs séparée avec des plages de dates.

Personnel est plus léger ici — peut-être un directeur général présenté en vedette ou un concierge pour la narration de la marque.

CTA est « Réserver Directement » — le pattern FME (Find, Match, Engage) qui donne aux clients une raison de réserver sur le site de l'hôtel lui-même plutôt que sur un OTA. Généralement une « garantie du meilleur tarif » ou une mise à niveau gratuite.

Cibles SEO locales : « hôtels à [ville] », « [nom hôtel] avis », « hôtel de charme [quartier] [ville] », « hôtels près de [point de repère] ».

Metadata JSONB stocke : équipements (piscine, spa, restaurant, salle de sport, recharge EV), attractions à proximité, calendrier des événements locaux, heures d'enregistrement/départ, politique concernant les animaux.

Variation industrielle 4 : Chaînes de cliniques vétérinaires

Les chaînes vétérinaires se développent rapidement en 2025 — la consolidation en médecine vétérinaire reflète ce qui s'est passé avec les DSOs dentaires il y a une décennie. La même architecture multi-sites s'applique parfaitement.

Services sont les services de soins pour animaux : examens de bien-être, vaccinations, nettoyage dentaire, chirurgie, soins d'urgence, pension, toilettage. Certains sites proposent des soins pour animaux exotiques ; la plupart non.

Personnel sont des vétérinaires avec expertise en espèces (petits animaux, équidés, animaux exotiques), certifications spécialisées et études.

CTA est « Prendre un rendez-vous » avec une touche — le formulaire d'admission devrait capturer les informations sur l'animal (espèce, race, âge, raison de la visite) pour acheminer correctement le rendez-vous.

Cibles SEO locales : « vétérinaire à [ville] », « vétérinaire urgence [ville] », « vétérinaire [espèce] [ville] », « nettoyage dentaire animaux [ville] ».

Metadata JSONB stocke : espèces acceptées, heures d'urgence (si différentes des heures régulières), capacité de pension, s'ils disposent d'un laboratoire sur place et d'imagerie.

Variation industrielle 5 : Chaînes de restaurants

Services deviennent les sections de menu : apéritifs, plats principaux, desserts, boissons. La chose critique ici est que la tarification peut varier selon le site. Un burger coûte 14 dollars à Austin et 19 dollars à Manhattan. La colonne metadata gère cela avec des remplacements de tarification spécifiques au site.

Personnel sont les chefs ou pit masters présentés — cela fonctionne mieux pour les marques où les gens derrière la nourriture font partie de l'histoire.

CTA est « Commander en ligne » — un lien conscient de la localisation qui route vers le bon système de commande en ligne (Toast, Square, ChowNow, ou personnalisé) pour le site le plus proche de l'utilisateur.

Cibles SEO locales : « [nom restaurant] [ville] menu », « restaurants près de moi », « restaurant [type de cuisine] [ville] », « [nom restaurant] horaires ».

Metadata JSONB stocke : rayon de livraison, disponibilité des réservations (avec lien OpenTable ou Resy), détails de stationnement, capacité de salle à manger privée, horaires d'happy hour.

Le tableau de comparaison d'architecture

Composant DSO dentaire Chaîne de salles de sport Groupe hôtelier Chaîne vétérinaire Restaurant
Étiquette « Services » Procédures Cours Types de chambres Services pour animaux Articles de menu
Étiquette « Personnel » Dentistes Entraîneurs Management Vétérinaires Chefs
CTA principal Prendre rendez-vous Adhérer Réserver une chambre Prendre rendez-vous Commander en ligne
Intégration de réservation NexHealth, Calendly Abonnements Stripe Personnalisé / Cloudbeds Personnalisé + admission animaux Toast, Square
Données locales clés Assurance, stationnement Horaire, équipement Équipements, attractions Espèces, heures urgence Tarification menu, livraison
Mot-clé SEO principal « dentiste à [ville] » « salle de sport près de moi » « hôtels à [ville] » « vétérinaire à [ville] » « [marque] [ville] menu »
Balisage de schéma Dentist, LocalBusiness SportsActivityLocation Hotel, LodgingBusiness VeterinaryCare Restaurant, Menu
Tables BD modifiées 0 0 0 0 0

Cette dernière ligne est le point. Zéro tables de base de données changent entre les industries. Vous utilisez les mêmes tables locations, services, staff, blog_posts, testimonials et events. Les étiquettes dans l'interface utilisateur changent. Les formes des métadonnées changent. L'architecture ne change pas.

Déploiement et performance à l'échelle

Nous déployons cela sur Vercel avec ISR (Incremental Static Regeneration). Chaque page de site est générée statiquement au moment de la construction et se revalide toutes les 60 secondes. Pour une chaîne de 200 sites, c'est 200 pages HTML statiques qui se chargent en moins d'une seconde sur n'importe quel appareil.

Les chiffres comptent. Voici ce que nous voyons généralement :

  • Temps de construction pour 200 sites : ~45 secondes sur Vercel Pro
  • TTFB par page de site : < 50ms (servi depuis le CDN de bordure)
  • Scores Lighthouse : 95+ partout
  • Revalidation ISR : 60 secondes stale-while-revalidate signifie que les mises à jour de contenu apparaissent dans la minute sans reconstruction complète

Ajouter un nouveau site est une insertion de base de données plus un appel de revalidation à la demande optionnel. Aucun déploiement nécessaire. La fonction generateStaticParams récupère les nouveaux sites lors de la prochaine construction ou du cycle ISR.

// Route API pour déclencher la revalidation quand un site est ajouté/mis à jour
import { revalidatePath } from 'next/cache'

export async function POST(request: Request) {
  const { slug } = await request.json()
  
  revalidatePath('/locations')
  revalidatePath(`/locations/${slug}`)
  
  return Response.json({ revalidated: true })
}

Répartition des coûts : Combien cela coûte réellement en 2025

Parlons de vrais chiffres. C'est une question commune que nous recevons lors de conversations de tarification.

Composant Coût mensuel (50 sites) Coût mensuel (200 sites)
Supabase Pro $25 $25 (même tier gère les deux)
Vercel Pro $20 $20
Bande passante Vercel (dépassement) ~$0 ~$40
Domaine + DNS (Cloudflare) $0 $0
CDN d'images (Cloudflare R2) ~$5 ~$15
Surveillance (Sentry) $26 $26
Infrastructure totale ~$76/mois ~$126/mois

Comparez cela à 50 sites WordPress distincts à ~30 $/mois chacun pour l'hébergement géré — c'est 1 500 $/mois avant même de penser à la maintenance, les licences de plugins ou la personne qui doit les maintenir à jour.

L'investissement en développement est plus élevé au départ — nous citons généralement des constructions multi-sites dans la fourchette de 30 000 $ à 80 000 $ selon la complexité — mais le coût opérationnel continu est une fraction de l'alternative WordPress multisite. Et vous ne payez pas 500 $/mois par site à un fournisseur de site Web de franchise qui vous enferme dans leur plateforme.

Pour les équipes intéressées par l'exploration d'intégrations CMS headless ou envisageant Astro au lieu de Next.js pour des constructions statiques encore plus rapides, la même architecture de base de données s'applique. Le framework frontal est interchangeable ; le modèle de données ne l'est pas.

FAQ

Cette architecture peut-elle gérer les sites dans différents fuseaux horaires ?

Absolument. La colonne hours JSONB stocke les heures d'exploitation de chaque site dans son fuseau horaire local. Nous incluons un champ timezone (par exemple « America/Chicago ») dans les métadonnées de l'emplacement et l'utilisons pour tout affichage sensible au temps comme les badges « Ouvert maintenant ». Tous les horodatages dans la base de données sont stockés en UTC et convertis sur le frontend.

Comment gérez-vous les sites qui offrent des services différents ?

C'est le pattern location_id nullable en action. Les services avec location_id = NULL sont partagés sur tous les sites — ils apparaissent sur la page de chaque site. Les services avec un location_id spécifique n'apparaissent que pour ce site. Vous pouvez également utiliser une table de jonction (location_services) pour les relations plusieurs-à-plusieurs si les services partagés ont besoin de remplacements spécifiques au site comme la tarification personnalisée ou la disponibilité.

Que se passe-t-il quand un nouveau site ouvre ?

Un administrateur du réseau ajoute le site via le tableau de bord. Cela crée une ligne dans la table locations, déclenche un webhook qui provoque la revalidation ISR, et la nouvelle page du site est en direct dans 60 secondes. Aucun développeur nécessaire, aucun déploiement, aucun changement DNS. Le site hérite immédiatement de tous les services et contenus partagés.

C'est mieux que WordPress Multisite pour les franchises ?

Pour la plupart des entreprises multi-sites, oui. WordPress Multisite était la réponse incontournable pendant une décennie, mais il a de vrais problèmes : une seule vulnérabilité de plugin peut faire tomber l'ensemble du réseau, la performance se dégrade au fur et à mesure que vous ajoutez des sites, et vous avez besoin d'un administrateur système dédié pour le maintenir en bon état. Cette architecture headless vous donne la performance du site statique, la sécurité au niveau de la base de données et zéro risque d'exécution partagé entre les sites.

Comment les responsables de site modifient-ils leur propre contenu sans casser les autres sites ?

Row-Level Security au niveau de la base de données garantit qu'un responsable de site à Austin ne peut littéralement pas voir ni modifier les données appartenant au site de Denver. Ce n'est pas appliqué par le code d'application qui pourrait avoir des bugs — c'est appliqué par Postgres lui-même. Même si l'interface d'administration avait un bug qui tentait d'interroger les données d'un autre site, la base de données retournerait des résultats vides.

Qu'en est-il du SEO — chaque site obtient-il son propre sitemap ?

Chaque page de site obtient sa propre entrée dans un sitemap dynamique unique généré au moment de la construction. Nous générons également des données structurées par site (JSON-LD) avec le schéma LocalBusiness, les géo-coordonnées, les heures d'exploitation et les types spécifiques à l'industrie. Google traite chaque page /locations/[slug] comme une liste d'entreprise locale distincte, ce qui est exactement ce que vous voulez pour les classements du pack local.

Les sites peuvent-ils avoir leurs propres articles de blog tout en partageant du contenu au niveau du réseau ?

Oui — c'est encore le pattern location_id nullable. Les articles de blog avec location_id = NULL apparaissent dans chaque flux de blog du site. Les publications avec un location_id spécifique n'apparaissent que dans le flux de ce site. Un site à Miami peut publier un article sur un événement communautaire local tandis que l'équipe d'entreprise publie une pensée d'leadership au niveau du réseau. Les deux apparaissent dans le flux de blog de Miami ; seul l'article d'entreprise apparaît partout ailleurs.

Combien coûte la maintenance continue comparée à la gestion de 50 sites distincts ?

Avec cette architecture, il y a une seule base de code, un seul déploiement et un seul ensemble de dépendances à maintenir. L'infrastructure mensuelle coûte 75-125 dollars selon l'échelle. Comparez cela à 50 installations WordPress : 1 500 $/mois juste pour l'hébergement, plus 10-20 heures par mois pour les mises à jour de plugins, les correctifs de sécurité et la résolution des problèmes du site qui s'est cassé après une mise à jour automatique. Nous avons vu des entreprises multi-sites réduire leur budget d'opérations Web annuel de 60-70% après migration vers ce pattern.