Crear una página personalizada
Qué es esto
upload-custom-page es la herramienta MCP que crea y actualiza páginas personalizadas: páginas cuyo cuerpo completo redactas tú mismo, como un fragmento de Astro/JSX o un documento HTML completo, con total libertad de diseño: tu propia estructura, CSS, animaciones e interactividad. La plataforma aporta el entorno (cabecera del documento, metadatos, tema) y elimina los elementos de documentación (sin tabla de contenidos, sin enlaces anterior/siguiente, excluidas de la búsqueda del sitio). La región del cuerpo es tuya.
Las páginas personalizadas están diseñadas para landing pages, tours de productos, páginas de precios, “sobre nosotros” y contacto: cualquier lugar donde el diseño sea más importante que la estructura del documento. Para contenido en Markdown, utiliza una página de conocimiento (create-page / update-page — consulta Tipos de página).
Cómo funciona
Una sola llamada crea y actualiza. upload-custom-page deriva un ID de documento determinista a partir del slug y realiza un upsert: si no existe una página para ese slug, crea un borrador; si ya existe una página personalizada, la actualiza. Cuando esa página está publicada, la edición se guarda como borrador mientras la URL activa sigue mostrando la versión actual. Las ediciones repetidas se consolidan en el mismo borrador. Nada de lo que subes es público hasta que se completa publish → publish-confirm (consulta Publicación). La convención conversacional es presentar el borrador (su contenido o draft_url) para su aprobación explícita antes de publish-confirm: publicar sin revisión es técnicamente posible, pero va en contra del flujo de trabajo documentado de la plataforma.
La tubería de carga se ejecuta en un orden fijo: salvaguarda de nombre de proyecto → verificación de idioma → protección is_faq → límite de tamaño de 2 MB → verificación de href bloqueados (se rechazan hrefs javascript:, data: y vbscript:) → lint (solo advertencias, nunca bloquea) → extracción de marcadores de traducción → upsert. Cualquier fallo rechaza toda la llamada con un mensaje preciso: Rechazos y soluciones de páginas personalizadas cataloga cada uno.
La traducción es automática. Envuelve cada cadena dirigida al visitante en un marcador — <T key="…" default="…"> en Astro/JSX, data-t-key en HTML — y la publicación traducirá automáticamente los valores del marcador, además del title y description, a cada idioma configurado para el módulo de páginas personalizadas. Nunca traduzcas manualmente ni subas copias por idioma: una página fuente se distribuye a todos los idiomas, cada uno en su propia URL — /{canonical_slug}/ para el idioma predeterminado, /{locale}/{canonical_slug}/ para el resto — con alternativas hreflang emitidas automáticamente una vez que hay dos o más variantes de idioma activas. El texto sin marcar nunca se traduce; se envía literalmente a todos los idiomas.
La calidad es parte del contrato: construye con una puntuación de Lighthouse de 95 o superior en las cuatro categorías (Rendimiento, Accesibilidad, Mejores Prácticas, SEO). Cuando se ejecuta el control previo a la publicación de la plataforma, las páginas personalizadas se auditan contra ese estándar y una puntuación insuficiente impide la publicación; existe una anulación force, y su uso queda registrado en el historial de auditoría. Las advertencias de lint en la respuesta de carga señalan los fallos de rendimiento más comunes: corrígelos antes de que te cuesten una publicación.
Cuándo usarla
- Estás creando una landing page, marketing, precios, “sobre nosotros” o contacto y quieres control total del marcado, estilo y comportamiento.
- Tienes una exportación “vibe-coded” (Lovable, v0, Bolt o HTML/Astro escrito a mano) para ponerla en vivo en un sitio de ComStack.
- La página necesita animación, interactividad personalizada o un diseño que Markdown no puede expresar.
- Estás cambiando texto, diseño o medios en una página personalizada existente: mismo
slug, nuevobody_source. - No es para documentación, guías o contenido de FAQ: las páginas de conocimiento obtienen traducción completa del cuerpo, JSON-LD de FAQ y búsqueda en el sitio; las páginas personalizadas, deliberadamente, no.
Parámetros
| Parámetro | Tipo | Requerido | Restricciones / predeterminado |
|---|---|---|---|
project_id | string | sí | El ID del proyecto, de list-my-projects o get-project-state. |
project_name | string | sí | Salvaguarda: debe coincidir exactamente con el nombre del proyecto (sin distinguir mayúsculas, tras recortar espacios). Una discrepancia rechaza la llamada. |
slug | string | sí | 1–256 caracteres. Slug de URL en minúsculas kebab-case; / anida segmentos; el literal index es la raíz del idioma. Reglas completas abajo. |
body_source | string | sí | El código fuente completo de la página: un fragmento Astro/JSX o un documento HTML. Límite: 2 MB. |
title | string | sí | 10–60 caracteres. Traducido automáticamente por idioma. |
description | string | sí | 50–160 caracteres. Traducido automáticamente por idioma. |
language | string | no | Idioma del contenido fuente BCP-47 (ej. es). Por defecto es el default_locale del proyecto. Debe ser uno de los idiomas configurados. Ignorado en actualizaciones: una página existente mantiene su idioma. |
canonical_slug | string | no | 1–256 caracteres. Por defecto es slug. Agrupa todas las variantes de idioma de una página bajo una identidad URL; se establece una vez, estable en todas las publicaciones. |
media_variants | object | no | { "<data-t-media key>": { "<locale>": "<URL absoluta>" } } — anulaciones de medios por idioma. Las claves deben coincidir con los marcadores data-t-media en el cuerpo; las URLs deben ser http(s) absolutas. |
schema_org_type | string | no | Tipo JSON-LD emitido para la página. Uno de WebPage, Article, AboutPage, ContactPage, CollectionPage, ProfilePage. Por defecto: WebPage. |
og_image | string | no | URL http(s) absoluta usada para og:image / twitter:image de la página. |
speakable_selector | string | no | Selectores CSS separados por comas para datos estructurados Speakable. Por defecto: [data-speakable], h1 + p. |
published_date | string | no | Fecha ISO 8601 — YYYY-MM-DD, sufijo de hora opcional. Emitido como datePublished. |
updated_date | string | no | Mismo formato. Emitido como dateModified. |
Reglas de slug y el ID del documento
- Cada segmento de slug separado por
/debe estar en minúsculas kebab-case: letras minúsculas, dígitos y guiones (about,solutions/voice,blog/buying-property-spain-2026). Mayúsculas, guiones bajos y espacios son rechazados. - La raíz del idioma (página de inicio) es el slug literal
index: un/vacío es rechazado. - La plataforma deriva el ID interno del documento a partir del slug: se eliminan las barras iniciales y finales, cada
/restante se convierte en-— slugsolutions/voice→ IDsolutions-voice. El ID es un detalle de almacenamiento; la URL siempre proviene del slug, con una barra final, y los idiomas traducidos obtienen su prefijo del enrutamiento (/es/solutions/voice/) — nunca escribas un prefijo de idioma en el slug mismo. - Después de que una página en vivo se edita y republica, su ID de documento rota.
slug+languagees el identificador duradero de una página; no almacenes IDs de documento. - Slug reservado: cuando el proyecto enruta su agente en vivo a una página de destino, ese slug configurado queda reservado y las subidas a él son rechazadas hasta que cambie la ubicación.
- Evita
/live/*— está reservado para la superficie del agente en vivo. Evita alojar páginas personalizadas bajo/docs/*para que no colisionen con el área de documentación; la única excepción permitida es la galería de ejemplos de la plataforma en/docs/custom-pages/examples/. Las subidas a estas rutas no están bloqueadas actualmente.
Formas del cuerpo: Astro/JSX o HTML
body_source se analiza de dos formas, elegidas por su estructura:
- Ruta Astro/JSX — el cuerpo comienza con una valla de frontmatter
---, o la secuencia de caracteres<Tseguida de espacio en blanco,/o>aparece en cualquier lugar de la fuente. La comprobación es un escaneo de texto plano, no consciente del análisis: un<Tliteral dentro de una cadena de JavaScript, un comentario HTML o un valor de atributo sigue enrutando todo el cuerpo al analizador JSX. Nombres de etiquetas más largos (<Title>,<Table>) no lo activan. - Ruta HTML — todo lo demás: un documento HTML completo o fragmento.
Qué significa “entregado tal como se redactó”, precisamente:
- En la ruta Astro/JSX, cada byte fuera de los elementos marcadores
<T>se conserva exactamente. La traducción inserta el texto en su lugar, y un cuerpo con cero marcadores se devuelve intacto. - En la ruta HTML, el cuerpo se analiza y se vuelve a serializar por idioma mediante un analizador HTML. La semántica se conserva, pero el marcado puede normalizarse (comillas de atributos, elementos implícitos como
<tbody>). Si los bytes exactos son importantes para ti, la ruta Astro/JSX es la que garantiza la exactitud de bytes.
En ambas formas, el cuerpo es marcado estático inyectado en el shell de la página. Nunca se compila ni ejecuta en el servidor: sin importaciones de componentes, sin TSX (no soportado en v1), sin ejecución de frontmatter. Omite la valla de frontmatter por completo: el cuerpo se envía literalmente, por lo que una valla --- aparecería como texto literal en la página publicada.
Un punto crítico en la ruta Astro/JSX: todo el cuerpo debe analizarse como JSX. Los bloques <style> o <script> sin procesar fallan el análisis tan pronto como su contenido llega a una llave {, y la subida es rechazada. Si tu página necesita bloques <style> o <script> en línea —la mayoría de las páginas estilizadas lo hacen—, redacta la forma HTML con marcadores data-t-key. En la ruta JSX, aplica estilos con clases, atributos style en línea o <link rel="stylesheet">, y carga comportamiento con <script defer src="…"></script>. El enrutamiento por escaneo de texto también funciona al revés: un cuerpo HTML cuyo script en línea contiene un <T literal (la secuencia <T más espacio en blanco, / o >) se enruta al analizador JSX y falla: mantén esa secuencia fuera de los scripts o divídela ('<' + 'T ').
Marcadores de traducción
Forma Astro/JSX — ambos funcionan:
<T key="hero.title" default="Build pages from a conversation" /><T key="hero.title">Build pages from a conversation</T>Forma HTML — el contenido de texto del elemento se convierte en el valor predeterminado:
<h1 data-t-key="hero.title">Build pages from a conversation</h1>Reglas:
- Las claves son literales de cadena estáticos que coinciden con
/^[a-z][a-z0-9_.]*[a-z0-9]$/: comienzan con una letra minúscula, terminan con una letra o dígito, solo puntos y guiones bajos en el medio — mínimo dos caracteres. El uso de espacios de nombres comohero.title,pricing.ctamantiene las páginas grandes manejables. - Cada marcador necesita un valor predeterminado: el atributo
defaulto texto interno estático (JSX), o contenido de texto plano no vacío (HTML). No hay elementos anidados dentro de un marcador, no hay marcadores dentro de marcadores, no hay{expresiones}. - La misma clave puede aparecer varias veces con el mismo valor predeterminado (se deduplica en una entrada); dos valores predeterminados diferentes para una misma clave son rechazados.
- La granularidad del marcador es el texto del elemento. Los valores de los atributos (
alt,placeholder,aria-label) no pueden llevar marcadores y se envían tal como se redactaron en cada idioma. - El mapa clave→valor predeterminado extraído se almacena con la página como
translatable_strings; la publicación traduce sus valores por idioma. Al renderizar, el contenedor JSX<T>se reemplaza por completo por la cadena del idioma; el elemento HTML permanece, condata-t-keyeliminado y su texto reemplazado. Un idioma al que le falte una clave recurre al valor predeterminado de la fuente.
Medios localizados
Marca cualquier elemento multimedia con data-t-media; su src en línea es el valor predeterminado para cada idioma:
<img src="https://res.cloudinary.com/acme/image/upload/f_auto,q_auto/v9/tf/hero.png" alt="Product screenshot" data-t-media="hero.shot" loading="lazy" />media_variants luego anula por idioma: { "hero.shot": { "es": "https://…/hero-es.png" } }. Las claves deben coincidir con un marcador data-t-media en el cuerpo; las URLs deben ser http(s) absolutas. Las URLs de medios se redactan, nunca se traducen automáticamente: un idioma sin anulación muestra el src en línea. El atributo data-t-media se elimina del marcado publicado. Usa la herramienta complementaria upload-media para alojar activos en la CDN de la plataforma con optimización automática de formato y calidad.
Lo que la plataforma fuerza — y rechaza
Forzado en cada subida:
metadata.typese establece encustomy la página se vincula a la plantillacustom-page. Esta herramienta no puede convertir una página personalizada en otro tipo de página.- La subida siempre llega como borrador; la URL en vivo permanece intacta hasta la publicación.
Rechazado, siempre:
- Una página de conocimiento en el mismo slug. Si el slug resuelve a una página Markdown existente, la subida se rechaza:
'pricing' (slug 'pricing') es una página de CONOCIMIENTO (markdown), no una página personalizada. upload-custom-page sobrescribiría su cuerpo con un body_source opaco. Edítala con update-page o elige un slug diferente para la página personalizada. is_faq. La herramienta no tiene tal parámetro, y colarlo en los argumentos sin procesar se detecta:is_faq=true está prohibido en docs de custom-page (template.forbids_is_faq=true). El JSON-LD de FAQ es un asunto de páginas de conocimiento.- Esquemas href peligrosos. Los valores
javascript:,data:yvbscript:en atributoshrefoxlink:href—incluyendo formas ofuscadas con entidades o espacios en blanco— son rechazados, y el error lista cada href infractor con su desplazamiento. Los atributossrcno se ven afectados, por lo que las imágenesdata:en línea pasan. - Cuerpos de más de 2 MB.
body_source tiene 2483911 bytes — por encima del límite estricto de 2 MB. Mueve los activos en línea grandes fuera de body_source (Cloudinary / estáticos).Aloja activos conupload-mediay haz referencia a ellos por URL.
Cadenas exactas y soluciones para cada rechazo: Rechazos y soluciones de páginas personalizadas.
Ejemplo
Una subida de sección de aterrizaje en forma Astro/JSX — tres marcadores de texto, una imagen localizable, anulación de medios por idioma, lint limpio:
{ "project_id": "k2xPq9wEfGhRtY3LmN8b", "project_name": "Acme Robotics", "slug": "solutions/voice", "title": "Voice agents for every channel", "description": "How Acme Robotics deploys multilingual voice agents across phone, web, and chat — with live demos and transparent pricing.", "schema_org_type": "Article", "og_image": "https://res.cloudinary.com/acme/image/upload/f_auto,q_auto/v9/tf/voice-card.png", "updated_date": "2026-06-10", "media_variants": { "hero.shot": { "es": "https://res.cloudinary.com/acme/image/upload/f_auto,q_auto/v9/tf/hero-es.png" } }, "body_source": "<section class=\"hero\" data-hero=\"true\">\n <h1><T key=\"hero.title\" default=\"Voice agents that speak every language\" /></h1>\n <p><T key=\"hero.sub\">Deploy once. Answer in thirty languages.</T></p>\n <img src=\"https://res.cloudinary.com/acme/image/upload/f_auto,q_auto/v9/tf/hero.png\" alt=\"Acme voice console\" data-t-media=\"hero.shot\" />\n <a class=\"cta\" href=\"/contact/\"><T key=\"hero.cta\" default=\"Talk to us\" /></a>\n</section>"}La respuesta de éxito:
{ "ok": true, "page": { "success": true, "path": "solutions-voice", "visibility": "draft", "draft_url": "<edit-mode preview link>", "next_step": "<review the draft, then publish>" }, "markers_extracted_count": 3, "lint_findings": [], "docs": { "authoring_guide": "transformento://docs/custom-pages/authoring", "troubleshooting": "transformento://docs/custom-pages/troubleshooting" }}markers_extracted_countcuenta las claves de marcadores de texto distintas —3aquí (hero.title,hero.sub,hero.cta); las clavesdata-t-mediason espacios para medios, no marcadores de texto.lint_findingses la matriz de advertencias consultivas; las entradas nunca bloquean.- Al actualizar una página en vivo,
pageinforma la acción del borrador —draft_created_from_live,draft_updated_from_live,updated_in_placeono_change— además de la ruta del borrador y un enlace de vista previadraft_url. La URL en vivo sigue sirviendo hasta que se completa la publicación.
Errores comunes
title must be between 10 and 60 characters/description must be between 50 and 160 characters— cuenta los caracteres, espacios incluidos; los límites cuentan la longitud de la cadena de JavaScript (unidades de código UTF-16), por lo que un guion largo es 1 carácter.language 'fr' is not in project.locales [en, es]…— añade el idioma al módulo de páginas personalizadas primero, u omitelanguagepara usar el predeterminado.Marker extraction failed (…)— cada problema de marcador se lista con su desplazamiento; corrige cada uno y vuelve a subir el cuerpo completo.body_source contains 1 blocked href scheme…— reemplaza los hrefjavascript:/data:con URLshttps:,mailto:,tel:, relativas o#ancla.'pricing' (slug 'pricing') es una página de CONOCIMIENTO (markdown)…— edita esa página conupdate-pageo elige otro slug.
Cada rechazo con su mensaje exacto y solución: Rechazos y soluciones de páginas personalizadas.
Relacionado
- Resumen de páginas personalizadas — qué son y cómo encajan las piezas.
- Rechazos y soluciones de páginas personalizadas — cada error, cadenas exactas, soluciones precisas.
- Tipos de página — páginas personalizadas vs de conocimiento, y qué herramienta edita cuál.
- Publicación — el flujo publish → publish-confirm, manifiestos y tokens de confirmación.
- Referencia de errores MCP — el sobre de errores de toda la plataforma y códigos de error compartidos.