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:writeScope
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
| Tarif | Requests/Min | Sites/Stunde |
|---|---|---|
| Free | 60 | 5 |
| Pro | 300 | 50 |
| Business | 1000 | 200 |
| Enterprise | Individuell | Unbegrenzt |
Das SDK respektiert automatisch den Retry-After Header bei einer 429-Antwort.
Tipps für große Batches
- Sequenziell, nicht parallel — Vermeide Race Conditions durch einzelne Site-Erstellung
- Fortschritt speichern — Speichere verarbeitete Elemente in einer Datei, um nach einem Absturz fortfahren zu können
- Input validieren — Prüfe Subdomains auf Gültigkeit (Kleinbuchstaben, keine Leerzeichen, max 63 Zeichen) vor dem API-Aufruf
- Dry-Run-Modus — Füge ein
--dry-runFlag 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
Restaurantseite erstellen
Erstelle eine komplette Restaurant-Website mit Menü, Buchungen, Bewertungen und Design Tokens über das OptimoCMS SDK, MCP und die REST API.
Webhook-Sync mit CRM
Halte dein CRM automatisch mit OptimoCMS über Webhooks synchron. Lerne, wie du Webhooks einrichtest, Payloads verarbeitest und Signaturen verifizierst.