Sitios estáticos
Publica un sitio ya construido —HTML, CSS, JS e imágenes— en tu subdominio de metsite.co, con versiones y vuelta atrás en un paso.
Si ya tienes el sitio hecho —a mano, con un generador como Astro, Vite o Hugo, o con un asistente—, lo subes tal cual y Meteor lo aloja en https://<slug>.metsite.co. Cada subida es una versión que no cambia; volver atrás es poner en línea una versión anterior, sin subir nada otra vez.
Son tres pasos: crear el sitio una vez → subir una versión cada vez que cambie → activarla (por defecto pasa sola al subirla).
Antes de empezar
| Scope | Para qué | Con una key de test |
|---|---|---|
sites:write | Crear el sitio, subir versiones y activarlas. | No: responde 403 test_mode_restricted, porque publica en internet. |
sites:read | Listar las versiones. | Sí |
Las rutas son planas (/sites/…): operan sobre el workspace de la key, y un sitio de otro workspace responde 404.
1. Crear el sitio
const sitio = await met.sites.createStatic({ name: 'Neuroliderazgo', slug: 'neuroliderazgo' });
// { site_id: 812, slug: 'neuroliderazgo', url: 'https://neuroliderazgo.metsite.co' }sitio = met.sites.create_static(name="Neuroliderazgo", slug="neuroliderazgo")curl -X POST https://api.met.meteor.com.co/api/v1/sites/static \
-H "Authorization: Bearer $METEOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Neuroliderazgo","slug":"neuroliderazgo"}'sluges el subdominio: de 3 a 40 caracteres, minúsculas, números y guiones sueltos (sin dos seguidos ni al principio o al final). Es único entre todos los sitios de Meteor: si está tomado responde409conflict, y algunos están reservados por la plataforma (400).- El sitio nace sin publicar:
urlresponde cuando haya una versión activa.
2. Subir una versión
Desde un zip, la forma más simple cuando ya tienes la carpeta construida (hasta 50 MB):
import { readFile } from 'node:fs/promises';
const zip = await readFile('sitio.zip');
const version = await met.sites.deployZip(sitio.site_id, zip);
// { id: '3f6c1e0a-…', status: 'ready', source: 'zip', file_count: 42, total_bytes: 5242880, is_active: true, url: 'https://neuroliderazgo.metsite.co' }with open("sitio.zip", "rb") as f:
version = met.sites.deploy_zip(sitio["site_id"], f.read())cd dist && zip -r ../sitio.zip . && cd ..
curl -X POST https://api.met.meteor.com.co/api/v1/sites/812/deploys \
-H "Authorization: Bearer $METEOR_API_KEY" \
-F file=@sitio.zipO archivo por archivo, en JSON: content en utf8 para el texto y en base64 para lo binario. El cuerpo entero tiene un tope de 10 MB; un sitio más grande va por zip.
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, extname } from 'node:path';
const TEXTO = new Set(['.html', '.css', '.js', '.mjs', '.json', '.svg', '.txt', '.xml', '.webmanifest', '.map']);
async function archivosDe(carpeta: string, raiz = carpeta) {
const out: { path: string; content: string; encoding: 'utf8' | 'base64' }[] = [];
for (const entrada of await readdir(carpeta, { withFileTypes: true })) {
const ruta = join(carpeta, entrada.name);
if (entrada.isDirectory()) out.push(...(await archivosDe(ruta, raiz)));
else {
const texto = TEXTO.has(extname(entrada.name));
const bytes = await readFile(ruta);
out.push({
path: relative(raiz, ruta).split('\\').join('/'),
content: texto ? bytes.toString('utf8') : bytes.toString('base64'),
encoding: texto ? 'utf8' : 'base64',
});
}
}
return out;
}
const version = await met.sites.deployFiles(812, { files: await archivosDe('dist') });import base64, pathlib
TEXTO = {".html", ".css", ".js", ".mjs", ".json", ".svg", ".txt", ".xml", ".webmanifest", ".map"}
raiz = pathlib.Path("dist")
files = []
for p in raiz.rglob("*"):
if p.is_file():
es_texto = p.suffix in TEXTO
files.append({
"path": p.relative_to(raiz).as_posix(),
"content": p.read_text("utf-8") if es_texto else base64.b64encode(p.read_bytes()).decode(),
"encoding": "utf8" if es_texto else "base64",
})
version = met.sites.deploy_files(812, files=files)Cada llamada es una versión completa: no se suman archivos a la anterior. Con activate: false (activate=False en Python) la versión se sube sin ponerla en línea, para revisarla o publicarla después.
Qué se acepta
| Regla | Tope |
|---|---|
| Zip | 50 MB |
| Archivos por versión | 2000 |
| Un archivo, descomprimido | 25 MB |
| Toda la versión, descomprimida | 200 MB |
JSON de deployFiles | 10 MB |
| Largo de una ruta | 512 caracteres |
- Extensiones:
html,htm,css,js,mjs,map,json,txt,xml,svg,png,jpg,jpeg,gif,webp,avif,ico,woff,woff2,ttf,otf,eot,mp4,webm,mp3,pdfywebmanifest. - Las rutas van sin
/al principio, sin.., sin\y sin carpetas ocultas (salvo.well-known/). Cada parte de la ruta admite letras sin tilde, números, espacios y. _ - @ + ( ) ! , = & $ '; una tilde o unañla rechaza. Lo más seguro: minúsculas, números,-,_y.. - Si todo viene dentro de una sola carpeta (
dist/), se quita:dist/index.htmlqueda comoindex.html. .DS_Storey__MACOSX/se ignoran.- Basta una ruta inválida para que no se suba nada. Responde
400deploy_files_rejectedcon la lista de rechazados enerrors, cada uno con supathy sureason.
Cómo se sirven las rutas
/sirveindex.html./contactosirvecontacto.htmlocontacto/index.html.- Una ruta que no existe sirve tu
404.html, con status 404. - El
robots.txty elsitemap.xmlque traiga tu sitio se sirven tal cual.
Versiones y vuelta atrás
const versiones = await met.sites.listDeploys(812, { limit: 10 });
// de la más reciente a la más vieja; is_active: true en la que está en línea
const anterior = versiones.find((v) => !v.is_active && v.status === 'ready');
if (anterior) await met.sites.activate(812, anterior.id);versiones = met.sites.list_deploys(812, limit=10)
anterior = next((v for v in versiones if not v["is_active"] and v["status"] == "ready"), None)
if anterior:
met.sites.activate(812, anterior["id"])curl -X POST https://api.met.meteor.com.co/api/v1/sites/812/deploys/3f6c1e0a-5b7d-4c2e-9a1f-0d8b7e6a5c4b/activate \
-H "Authorization: Bearer $METEOR_API_KEY"status | Qué es |
|---|---|
ready | Subida completa: se puede activar. |
uploading | Todavía se está subiendo. |
failed | La subida no terminó; error dice por qué. Sube otra versión. |
listDeploys trae 20 por defecto (de 1 a 100). activate solo pone en línea una versión ready de ese sitio; la primera activación publica el sitio. El cambio es inmediato y no se sube nada otra vez.
Tu dominio
El sitio vive en https://<slug>.metsite.co. Conectar un dominio propio (www.tunegocio.com) no se hace por la API.
Desde un asistente (MCP)
Estas operaciones se alcanzan con la herramienta genérica del servidor MCP (met_call_operation), en su forma por archivos: el zip no viaja por MCP, porque un asistente no adjunta binarios en la conversación.
Errores comunes
| Código | Qué pasó |
|---|---|
400 validation_error | El slug no cumple el formato, o está reservado. |
400 deploy_files_rejected | Alguna ruta o extensión no se acepta; mira errors. No se subió nada. |
400 deploy_zip_missing | El zip no llegó en el campo multipart file. |
400 zip_invalid | El archivo no es un zip válido. |
400 deploy_empty · deploy_too_many_files · deploy_too_large · deploy_zip_too_large | La versión no trae archivos o pasa uno de los topes. |
413 FILE_TOO_LARGE | El zip subido por multipart pasa de 50 MB. |
403 test_mode_restricted | Una key met_test_ no publica sitios. |
404 not_found · deploy_not_found | El sitio no es de este workspace, o la versión no es de este sitio. |
409 conflict | El slug ya está en uso. |
409 site_not_static | El sitio se arma por bloques en el panel: las versiones son solo para sitios creados con createStatic. |
409 deploy_not_ready | Se intentó activar una versión que no está ready. |
503 deploy_upload_failed | La subida falló de nuestro lado. La versión queda failed y la que estaba en línea sigue en línea: vuelve a subirla. |