OptimoCMSDocs
Guides

Multi-site automatisering

Maak en beheer tientallen sites programmatisch met de OptimoCMS SDK. Ideaal voor franchises, agencies en white-label platforms.

Multi-site beheer automatiseren

Heb je een franchise met 50 vestigingen die elk een eigen website nodig hebben? Of beheer je als agency tientallen klant-sites? Met de OptimoCMS SDK automatiseer je het volledige proces.

In deze tutorial leer je:

  • Een script schrijven dat meerdere sites aanmaakt in een loop
  • Rate limits respecteren met ingebouwde retry-logica
  • Foutafhandeling voor robuuste batch-operaties
  • Design tokens en content per vestiging aanpassen

Vereisten

  • Node.js 18+
  • npm install @optimocms/sdk
  • API key met sites:write scope

Stap 1 — Vestigingsdata voorbereiden

Maak een JSON-bestand met alle vestigingen:

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

Stap 2 — Batch-script met rate limit respect

De SDK heeft ingebouwde retry-logica voor 429 Too Many Requests. Je kunt daarnaast zelf een delay toevoegen tussen requests om ruim binnen de limiet te blijven.

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> {
  // 1. Site aanmaken
  const site = await client.sites.create({
    name: branch.name,
    subdomain: branch.subdomain,
    language: 'nl',
  });

  // 2. Template installeren
  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',
    },
  });

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

  // 4. Publiceren
  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}`);
    }

    // Respecteer rate limits: 500ms pauze tussen sites
    await sleep(500);
  }

  // Rapport
  const success = results.filter(r => r.siteId).length;
  const failed = results.filter(r => r.error).length;
  console.log(`\nKlaar: ${success} gelukt, ${failed} mislukt van ${branches.length} totaal`);

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

main().catch(console.error);

Stap 3 — Foutafhandeling en retry

De SDK handelt rate limits automatisch af (exponential backoff, max 3 retries). Voor andere fouten kun je een retry-wrapper toevoegen:

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) {
        // SDK retries dit automatisch, maar als het toch doorsijpelt:
        const waitMs = error.retryAfter ? error.retryAfter * 1000 : delayMs * attempt;
        console.log(`Rate limited, wacht ${waitMs}ms...`);
        await sleep(waitMs);
        continue;
      }

      if (attempt === maxAttempts) throw error;

      console.log(`Poging ${attempt} mislukt, retry in ${delayMs * attempt}ms...`);
      await sleep(delayMs * attempt);
    }
  }
  throw new Error('Unreachable');
}

// Gebruik:
const siteId = await withRetry(() => createBranchSite(branch));

Stap 4 — Bulk design tokens updaten

Bestaande sites in bulk bijwerken met een nieuw kleurschema:

async function updateAllBrandColors(newColor: string) {
  // Paginate door alle sites
  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} bijgewerkt`);
    await sleep(200);
  }
}

// Franchise rebranding: van rood naar blauw
await updateAllBrandColors('#1D4ED8');

Stap 5 — MCP variant

Met MCP kun je hetzelfde bereiken via natuurlijke taal in Cursor of Claude:

Maak 3 sites aan voor Pizza Roma franchise:
- Pizza Roma Amsterdam (subdomain: pizzaroma-amsterdam)
- Pizza Roma Rotterdam (subdomain: pizzaroma-rotterdam)
- Pizza Roma Utrecht (subdomain: pizzaroma-utrecht)

Gebruik voor elke site het restaurant-template met primary kleur #D4380D en font Poppins.
Maak een contactpagina met het adres en telefoonnummer per vestiging.
Publiceer alle sites.

Rate limits per tier

TierRequests/minSites/uur
Free605
Pro30050
Business1000200
EnterpriseCustomOnbeperkt

De SDK respecteert automatisch de Retry-After header bij een 429-response.


Tips voor grote batches

  1. Sequentieel, niet parallel — Voorkom race conditions door sites één voor één aan te maken
  2. Checkpoint je voortgang — Sla verwerkte items op in een bestand, zodat je kunt hervatten na een crash
  3. Valideer input — Check subdomains op geldigheid (lowercase, geen spaties, max 63 tekens) vóór het aanroepen van de API
  4. Dry-run modus — Voeg een --dry-run flag toe die alleen logt wat er zou gebeuren
const DRY_RUN = process.argv.includes('--dry-run');

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

Volgende stappen

On this page