OptimoCMSDocs
Guides

Multi-Site-Automatisierung

Erstelle und verwalte Dutzende von Sites programmatisch mit dem OptimoCMS SDK. Ideal für Franchises, Agenturen und White-Label-Plattformen.

Multi-Site-Verwaltung automatisieren

Hast du eine Franchise mit 50 Standorten, die jeweils eine eigene Website benötigen? Oder verwaltest du als Agentur Dutzende von Kunden-Sites? Mit dem OptimoCMS SDK automatisierst du den gesamten Prozess.

In diesem Tutorial lernst du:

  • Ein Skript schreiben, das mehrere Sites in einer Schleife erstellt
  • Rate Limits mit eingebauter Retry-Logik respektieren
  • Fehlerbehandlung für robuste Batch-Operationen
  • Design Tokens und Inhalte pro Standort anpassen

Voraussetzungen

  • Node.js 18+
  • npm install @optimocms/sdk
  • API-Schlüssel mit sites:write Scope

Schritt 1 — Standortdaten vorbereiten

Erstelle eine JSON-Datei mit allen Standorten:

[
  {
    "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"
  }
]

Schritt 2 — Batch-Skript mit Rate-Limit-Beachtung

Das SDK hat eingebaute Retry-Logik für 429 Too Many Requests. Du kannst zusätzlich eine Verzögerung zwischen den Requests einbauen.

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: 'Kontakt',
    slug: 'kontakt',
    status: 'published',
    blocks: [
      {
        type: 'Hero',
        props: { title: `Besuche ${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(`\nFertig: ${success} erfolgreich, ${failed} fehlgeschlagen von ${branches.length} gesamt`);

  if (failed > 0) {
    console.log('\nFehlgeschlagene Standorte:');
    results.filter(r => r.error).forEach(r => {
      console.log(`  - ${r.branch}: ${r.error}`);
    });
  }
}

main().catch(console.error);

Schritt 3 — Fehlerbehandlung und Retry

Das SDK behandelt Rate Limits automatisch (exponential Backoff, max 3 Retries). Für andere Fehler kannst du einen Retry-Wrapper hinzufügen:

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(`Rate Limited, warte ${waitMs}ms...`);
        await sleep(waitMs);
        continue;
      }

      if (attempt === maxAttempts) throw error;

      console.log(`Versuch ${attempt} fehlgeschlagen, Retry in ${delayMs * attempt}ms...`);
      await sleep(delayMs * attempt);
    }
  }
  throw new Error('Unreachable');
}

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

Schritt 4 — Design Tokens in Bulk aktualisieren

Bestehende Sites in Bulk mit einem neuen Farbschema aktualisieren:

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} aktualisiert`);
    await sleep(200);
  }
}

await updateAllBrandColors('#1D4ED8');

Schritt 5 — MCP-Variante

Mit MCP kannst du dasselbe über natürliche Sprache in Cursor oder Claude erreichen:

Erstelle 3 Sites für die Pizza Roma Franchise:
- Pizza Roma Amsterdam (Subdomain: pizzaroma-amsterdam)
- Pizza Roma Rotterdam (Subdomain: pizzaroma-rotterdam)
- Pizza Roma Utrecht (Subdomain: pizzaroma-utrecht)

Verwende für jede Site das Restaurant-Template mit Primärfarbe #D4380D und Schrift Poppins.
Erstelle eine Kontaktseite mit Adresse und Telefonnummer pro Standort.
Veröffentliche alle Sites.

Rate Limits pro Tarif

TarifRequests/MinSites/Stunde
Free605
Pro30050
Business1000200
EnterpriseIndividuellUnbegrenzt

Das SDK respektiert automatisch den Retry-After Header bei einer 429-Antwort.


Tipps für große Batches

  1. Sequenziell, nicht parallel — Vermeide Race Conditions durch einzelne Site-Erstellung
  2. Fortschritt speichern — Speichere verarbeitete Elemente in einer Datei, um nach einem Absturz fortfahren zu können
  3. Input validieren — Prüfe Subdomains auf Gültigkeit (Kleinbuchstaben, keine Leerzeichen, max 63 Zeichen) vor dem API-Aufruf
  4. Dry-Run-Modus — Füge ein --dry-run Flag hinzu, das nur loggt, was passieren würde
const DRY_RUN = process.argv.includes('--dry-run');

if (DRY_RUN) {
  console.log(`[DRY RUN] Würde erstellen: ${branch.name} → ${branch.subdomain}`);
} else {
  await createBranchSite(branch);
}

Nächste Schritte

  • CI/CD Pipeline — Dieses Skript über GitHub Actions automatisieren
  • Webhook Sync — Benachrichtigungen erhalten, wenn Sites veröffentlicht werden
  • SDK Pagination — Effizient durch große Listen iterieren

On this page