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:writescope
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
| Tier | Requests/min | Sites/uur |
|---|---|---|
| Free | 60 | 5 |
| Pro | 300 | 50 |
| Business | 1000 | 200 |
| Enterprise | Custom | Onbeperkt |
De SDK respecteert automatisch de Retry-After header bij een 429-response.
Tips voor grote batches
- Sequentieel, niet parallel — Voorkom race conditions door sites één voor één aan te maken
- Checkpoint je voortgang — Sla verwerkte items op in een bestand, zodat je kunt hervatten na een crash
- Valideer input — Check subdomains op geldigheid (lowercase, geen spaties, max 63 tekens) vóór het aanroepen van de API
- Dry-run modus — Voeg een
--dry-runflag 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
- CI/CD pipeline — Automatiseer dit script via GitHub Actions
- Webhook sync — Ontvang notificaties wanneer sites gepubliceerd worden
- SDK Pagination — Efficiënt door grote lijsten itereren
Restaurantsite bouwen
Maak een complete restaurantwebsite met menu, boekingen, reviews en design tokens via de OptimoCMS SDK, MCP en REST API.
Webhook sync met CRM
Houd je CRM automatisch gesynchroniseerd met OptimoCMS via webhooks. Leer hoe je webhooks instelt, payloads verwerkt en signatures verifieert.