OptimoCMSDocs
Guides

Automatisation multi-sites

Créez et gérez des dizaines de sites de manière programmatique avec le SDK OptimoCMS. Idéal pour les franchises, agences et plateformes white-label.

Automatiser la gestion multi-sites

Vous avez une franchise avec 50 emplacements qui ont chacun besoin de leur propre site web ? Ou vous gérez des dizaines de sites clients en tant qu'agence ? Le SDK OptimoCMS vous permet d'automatiser l'ensemble du processus.

Dans ce tutoriel, vous apprendrez à :

  • Écrire un script qui crée plusieurs sites dans une boucle
  • Respecter les limites de requêtes avec la logique de retry intégrée
  • Gérer les erreurs pour des opérations batch robustes
  • Personnaliser les design tokens et le contenu par emplacement

Prérequis

  • Node.js 18+
  • npm install @optimocms/sdk
  • Clé API avec scope sites:write

Étape 1 — Préparer les données des emplacements

Créez un fichier JSON avec tous les emplacements :

[
  {
    "name": "Pizza Roma - Amsterdam",
    "subdomain": "pizzaroma-amsterdam",
    "address": "Keizersgracht 123, Amsterdam",
    "phone": "+31 20 123 4567",
    "color": "#D4380D"
  },
  {
    "name": "Pizza Roma - Rotterdam",
    "subdomain": "pizzaroma-rotterdam",
    "address": "Coolsingel 45, Rotterdam",
    "phone": "+31 10 987 6543",
    "color": "#D4380D"
  },
  {
    "name": "Pizza Roma - Utrecht",
    "subdomain": "pizzaroma-utrecht",
    "address": "Oudegracht 78, Utrecht",
    "phone": "+31 30 456 7890",
    "color": "#D4380D"
  }
]

Étape 2 — Script batch avec respect des limites

Le SDK intègre une logique de retry pour les 429 Too Many Requests. Vous pouvez aussi ajouter un délai entre les requêtes.

import { OptimoCMS, RateLimitError } from '@optimocms/sdk';
import { readFileSync } from 'node:fs';

interface Branch {
  name: string;
  subdomain: string;
  address: string;
  phone: string;
  color: string;
}

const client = new OptimoCMS({
  apiKey: process.env.OPTIMOCMS_API_KEY!,
  maxRetries: 3,
});

const branches: Branch[] = JSON.parse(
  readFileSync('./branches.json', 'utf-8')
);

async function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function createBranchSite(branch: Branch): Promise<string> {
  const site = await client.sites.create({
    name: branch.name,
    subdomain: branch.subdomain,
    language: 'nl',
  });

  const templates = await client.templates.list({ category: 'restaurant' });
  await client.templates.install(site.id, {
    templateId: templates.data[0].id,
    designTokens: {
      colorPrimary: branch.color,
      fontHeading: 'Poppins',
      fontBody: 'Inter',
      radiusCard: '16px',
      radiusButton: '8px',
    },
  });

  await client.pages.create(site.id, {
    title: 'Contact',
    slug: 'contact',
    status: 'published',
    blocks: [
      {
        type: 'Hero',
        props: { title: `Visitez ${branch.name}`, subtitle: branch.address },
      },
      {
        type: 'ContactInfo',
        props: {
          address: branch.address,
          phone: branch.phone,
          email: `info@${branch.subdomain}.nl`,
        },
      },
      {
        type: 'BookingWidget',
        props: { serviceId: 'dinner' },
      },
    ],
  });

  await client.sites.publish(site.id);
  return site.id;
}

async function main() {
  const results: Array<{ branch: string; siteId?: string; error?: string }> = [];

  for (const branch of branches) {
    try {
      const siteId = await createBranchSite(branch);
      results.push({ branch: branch.name, siteId });
      console.log(`✓ ${branch.name} → ${branch.subdomain}.optimocms.com`);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      results.push({ branch: branch.name, error: message });
      console.error(`✗ ${branch.name}: ${message}`);
    }

    await sleep(500);
  }

  const success = results.filter(r => r.siteId).length;
  const failed = results.filter(r => r.error).length;
  console.log(`\nTerminé : ${success} réussis, ${failed} échoués sur ${branches.length} au total`);
}

main().catch(console.error);

Étape 3 — Gestion des erreurs et retry

Le SDK gère les limites de requêtes automatiquement (backoff exponentiel, max 3 retries). Pour d'autres erreurs, ajoutez un wrapper de retry :

async function withRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 3,
  delayMs = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error instanceof RateLimitError) {
        const waitMs = error.retryAfter ? error.retryAfter * 1000 : delayMs * attempt;
        console.log(`Limite atteinte, attente ${waitMs}ms...`);
        await sleep(waitMs);
        continue;
      }

      if (attempt === maxAttempts) throw error;

      console.log(`Tentative ${attempt} échouée, retry dans ${delayMs * attempt}ms...`);
      await sleep(delayMs * attempt);
    }
  }
  throw new Error('Unreachable');
}

const siteId = await withRetry(() => createBranchSite(branch));

Étape 4 — Mise à jour en masse des design tokens

Mettre à jour les sites existants en masse avec un nouveau schéma de couleurs :

async function updateAllBrandColors(newColor: string) {
  for await (const site of client.sites.list()) {
    if (!site.subdomain.startsWith('pizzaroma-')) continue;

    await client.designTokens.update(site.id, {
      colorPrimary: newColor,
    });

    console.log(`✓ ${site.name} mis à jour`);
    await sleep(200);
  }
}

await updateAllBrandColors('#1D4ED8');

Étape 5 — Variante MCP

Avec MCP, vous pouvez obtenir le même résultat en langage naturel dans Cursor ou Claude :

Créez 3 sites pour la franchise Pizza Roma :
- Pizza Roma Amsterdam (sous-domaine : pizzaroma-amsterdam)
- Pizza Roma Rotterdam (sous-domaine : pizzaroma-rotterdam)
- Pizza Roma Utrecht (sous-domaine : pizzaroma-utrecht)

Utilisez le template restaurant pour chaque site avec couleur primaire #D4380D et police Poppins.
Créez une page contact avec l'adresse et le numéro de téléphone par emplacement.
Publiez tous les sites.

Limites par forfait

ForfaitRequêtes/minSites/heure
Free605
Pro30050
Business1000200
EnterpriseSur mesureIllimité

Le SDK respecte automatiquement le header Retry-After lors d'une réponse 429.


Conseils pour les gros batches

  1. Séquentiel, pas parallèle — Évitez les race conditions en créant les sites un par un
  2. Sauvegardez votre progression — Enregistrez les éléments traités pour pouvoir reprendre après un crash
  3. Validez les entrées — Vérifiez les sous-domaines (minuscules, pas d'espaces, max 63 caractères) avant d'appeler l'API
  4. Mode dry-run — Ajoutez un flag --dry-run qui ne fait que logger ce qui se passerait
const DRY_RUN = process.argv.includes('--dry-run');

if (DRY_RUN) {
  console.log(`[DRY RUN] Créerait : ${branch.name} → ${branch.subdomain}`);
} else {
  await createBranchSite(branch);
}

Prochaines étapes

On this page