Automatización multi-sitio
Crea y gestiona decenas de sitios programáticamente con el SDK de OptimoCMS. Ideal para franquicias, agencias y plataformas white-label.
Automatizar la gestión multi-sitio
¿Tienes una franquicia con 50 ubicaciones que necesitan cada una su propio sitio web? ¿O gestionas decenas de sitios de clientes como agencia? El SDK de OptimoCMS te permite automatizar todo el proceso.
En este tutorial aprenderás a:
- Escribir un script que crea múltiples sitios en un bucle
- Respetar los límites de peticiones con lógica de retry integrada
- Manejar errores para operaciones batch robustas
- Personalizar design tokens y contenido por ubicación
Requisitos previos
- Node.js 18+
npm install @optimocms/sdk- Clave API con scope
sites:write
Paso 1 — Preparar datos de ubicaciones
Crea un archivo JSON con todas las ubicaciones:
[
{
"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"
}
]Paso 2 — Script batch con respeto de límites
El SDK tiene lógica de retry integrada para 429 Too Many Requests. También puedes añadir un retraso entre peticiones.
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: 'Contacto',
slug: 'contacto',
status: 'published',
blocks: [
{
type: 'Hero',
props: { title: `Visita ${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(`\nListo: ${success} exitosos, ${failed} fallidos de ${branches.length} total`);
}
main().catch(console.error);Paso 3 — Manejo de errores y retry
El SDK maneja los límites de peticiones automáticamente (backoff exponencial, máx. 3 retries). Para otros errores puedes añadir 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(`Límite alcanzado, esperando ${waitMs}ms...`);
await sleep(waitMs);
continue;
}
if (attempt === maxAttempts) throw error;
console.log(`Intento ${attempt} fallido, reintentando en ${delayMs * attempt}ms...`);
await sleep(delayMs * attempt);
}
}
throw new Error('Unreachable');
}
const siteId = await withRetry(() => createBranchSite(branch));Paso 4 — Actualizar design tokens en masa
Actualizar sitios existentes en masa con un nuevo esquema de colores:
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} actualizado`);
await sleep(200);
}
}
await updateAllBrandColors('#1D4ED8');Paso 5 — Variante MCP
Con MCP puedes lograr lo mismo en lenguaje natural en Cursor o Claude:
Crea 3 sitios para la franquicia Pizza Roma:
- Pizza Roma Amsterdam (subdominio: pizzaroma-amsterdam)
- Pizza Roma Rotterdam (subdominio: pizzaroma-rotterdam)
- Pizza Roma Utrecht (subdominio: pizzaroma-utrecht)
Usa la plantilla de restaurante para cada sitio con color primario #D4380D y fuente Poppins.
Crea una página de contacto con la dirección y teléfono por ubicación.
Publica todos los sitios.Límites por plan
| Plan | Peticiones/min | Sitios/hora |
|---|---|---|
| Free | 60 | 5 |
| Pro | 300 | 50 |
| Business | 1000 | 200 |
| Enterprise | Personalizado | Ilimitado |
El SDK respeta automáticamente el header Retry-After en una respuesta 429.
Consejos para grandes batches
- Secuencial, no paralelo — Evita race conditions creando sitios de uno en uno
- Guarda tu progreso — Almacena elementos procesados en un archivo para poder reanudar tras un fallo
- Valida la entrada — Comprueba subdominios (minúsculas, sin espacios, máx. 63 caracteres) antes de llamar a la API
- Modo dry-run — Añade un flag
--dry-runque solo registra lo que sucedería
const DRY_RUN = process.argv.includes('--dry-run');
if (DRY_RUN) {
console.log(`[DRY RUN] Crearía: ${branch.name} → ${branch.subdomain}`);
} else {
await createBranchSite(branch);
}Siguientes pasos
- Pipeline CI/CD — Automatizar este script vía GitHub Actions
- Sync webhook — Recibir notificaciones cuando los sitios se publican
- Paginación SDK — Iterar eficientemente a través de grandes listas
Crear un sitio de restaurante
Crea un sitio web de restaurante completo con menú, reservas, reseñas y design tokens usando el SDK de OptimoCMS, MCP y la API REST.
Sync webhook con CRM
Mantén tu CRM sincronizado automáticamente con OptimoCMS mediante webhooks. Aprende a configurar webhooks, procesar payloads y verificar firmas.