Superar los controles de calidad de páginas personalizadas
Qué es esto
El contrato de calidad para páginas personalizadas: lo que la plataforma verifica entre upload-custom-page y una URL activa, y cómo crear una página que supere todo en la primera publicación. El contrato en una frase: construye cada página personalizada para obtener al menos 95 puntos en las cuatro categorías de Lighthouse: rendimiento, accesibilidad, mejores prácticas y SEO. Las páginas personalizadas envían tu marcado tal como fue creado, por lo que la puntuación depende totalmente de ti; esta página enumera las comprobaciones y las tácticas para superarlas.
Cómo funciona
La calidad se aplica en tres niveles:
1. Lint en el momento de la carga (informativo). upload-custom-page devuelve una matriz lint_findings. Cada hallazgo es una advertencia (los hallazgos nunca bloquean la carga), pero cada uno se asigna directamente a los puntos de Lighthouse que perderías más tarde. Trata un lint_findings no vacío como tu lista de tareas previa a la auditoría.
2. Auditoría de Lighthouse previa a la publicación. Cuando se ejecuta el control de calidad previo a la publicación, cada página personalizada pendiente se audita con Lighthouse en Chrome headless en las categorías de performance, accessibility, best-practices y seo. Cada categoría debe obtener al menos 95 puntos, o la publicación fallará: publish-status informa status: "failed" con un hallazgo por cada URL fallida —
Lighthouse pre-publish gate failed: https://www.brew.example/subscriptions scored below 95 (perf=88, a11y=96, bp=100, seo=100)— además de una lista de remediación que nombra cada auditoría fallida. Si el control se ejecuta para una publicación determinada es una decisión de la plataforma; el contrato de creación es incondicional: construye para ≥95. publish-confirm acepta un indicador force que permite publicar superando puntuaciones bajas; cada aprobación forzada se registra, con sus puntuaciones, en el registro de auditoría del proyecto. Úsalo para una emergencia real, no como hábito. Una nota honesta sobre la medición: esta auditoría del lado de la plataforma es el control previo a la publicación, y no existe un endpoint de auditoría de borradores; para ver las puntuaciones antes de publicar, renderiza el cuerpo tú mismo y ejecuta Lighthouse localmente; de lo contrario, la primera publicación actúa como la primera auditoría.
3. La garantía de renderizado opaco. Las páginas personalizadas se renderizan a través de una ruta opaca dedicada: el cuerpo se incrusta en el shell de la página (head, tema, chrome) y nunca es procesado por la canalización de markdown, por lo que las clases, los atributos data-*, los estilos en línea y los scripts permanecen intactos. El despliegue verifica esto para cada página personalizada en el sitio construido y cancela la publicación si alguna página personalizada se renderizó a través de la canalización de markdown en su lugar: un renderizado roto nunca reemplaza a una página funcional.
Cuándo usarlo
- Antes de cargar una exportación generada por IA (Lovable, v0, Bolt): para eliminar los hábitos que cuestan puntos en Lighthouse.
- Una publicación falló con
Lighthouse pre-publish gate failedy necesitas corregir las auditorías nombradas. upload-custom-pagedevolviólint_findingsy quieres saber qué advertencias importan.- Estás configurando metadatos AEO (
speakable_selector,schema_org_type,og_image) y quieres los valores predeterminados. - Tu proyecto habilita CSP y quieres páginas que sigan funcionando bajo esa política.
El contrato, punto por punto
Rendimiento ≥ 95
- Un archivo, CSS en línea. Coloca el CSS crítico en un bloque
<style>en el cuerpo. No uses hojas de estilo externas que bloqueen el renderizado; si debes enlazar una, usamedia="print" onload="this.media='all'"o un patrón de precarga. - Diferir, asíncrono o eliminar JavaScript. Cada
<script>debe llevardefer(preferido) oasync. Las páginas con mayor puntuación no envían JS en absoluto: la animación y la interactividad funcionan muy bien solo con CSS. (Los scriptstype="module", JSON-LD y las plantillas ya no bloquean). - Fuentes del sistema o
font-display: swap. Una pila de fuentessystem-uicuesta cero solicitudes. Si cargas una fuente personalizada, cada bloque@font-facenecesitafont-display: swap, o el texto será invisible mientras se carga la fuente. - Dimensiona cada imagen. Los atributos explícitos
widthyheightevitan el cambio de diseño (CLS). - Optimiza cada imagen. Aloja a través de
upload-media: devuelve URLs de Cloudinary conf_auto,q_auto(formatos modernos, calidad ajustada). La plataforma nunca reescribe tus URLs, por lo que las banderas de optimización deben estar en la URL que creas. - Carga diferida (lazy-load) debajo del pliegue, mantén el hero ansioso.
loading="lazy"en cada imagen excepto en la imagen LCP/hero; marca el hero (o un elemento envolvente) con un atributodata-heropara que el lint sepa que la carga ansiosa es deliberada. - Scroll-reveal: envía el contenido visible, ocúltalo con JS. Nunca envíes contenido con
opacity: 0por defecto. Haz que el script añada la clase que habilita los estados oculto/visible, para que los visitantes sin JS, los usuarios con movimiento reducido y el rastreo de Lighthouse vean el contenido completo.
Accesibilidad ≥ 95
- El shell de la plataforma ya renderiza un punto de referencia
<main>(conlang/dir) y el meta viewport alrededor de cada cuerpo de página personalizada; no añadas tu propio<main>, o la página fallará en accesibilidad por un punto de referencia duplicado. Estructura el cuerpo con envoltorios<div>y<section>(más<nav>donde corresponda), un<h1>, niveles de encabezado secuenciales; los encabezados siguen comenzando en<h1>dentro del cuerpo. - Siempre pinta tus propios fondos. El fondo del shell es el tema del proyecto (que varía y puede ser oscuro o un degradado según la hora del día), por lo que una sección que no establezca un fondo explícito puede fallar silenciosamente en el contraste.
- Texto
alten cada imagen (soloalt=""vacío para decoración pura). - Contraste de color de al menos 4.5:1 para el texto del cuerpo.
- Elementos interactivos reales:
<a>y<button>, no<div>con manejadores de clic, con estados:focus-visiblevisibles.
Accesibilidad de contenido dinámico. Las calculadoras y otros widgets interactivos necesitan tres cosas: renderizar los resultados en una región aria-live (o role="status") para que se anuncie la actualización, establecer aria-invalid en las entradas que fallan la validación y mover el foco al primer campo no válido.
Mejores prácticas y SEO ≥ 95
- El shell de la plataforma proporciona el encabezado del documento: tu
title(10–60 caracteres) ydescription(50–160 caracteres) se convierten en las metaetiquetas, y las URLs canónicas, hreflang y el sitemap se generan automáticamente. Proporciona un título y una descripción que valga la pena posicionar, y la categoría SEO se encargará de sí misma. - Usa URLs
httpsabsolutas para todos los activos externos.
Reglas de lint en el momento de la carga
| regla | se dispara cuando | solución |
|---|---|---|
body-too-large | cuerpo sobre el límite suave de 500 KB (2 MB es rechazo duro) | mueve activos en línea a URLs de upload-media o alojamiento estático |
no-eager-script | <script> sin defer/async | añade defer |
img-not-lazy | <img> sin loading="lazy" y sin data-hero cercano | usa carga diferida o marca el hero con data-hero |
blocking-stylesheet | <link rel="stylesheet"> sin media o precarga | inserta el CSS en línea o usa el patrón media="print" |
no-font-display-swap | @font-face sin font-display: swap | añade font-display: swap; |
cloudinary-not-auto | URL de Cloudinary sin f_auto/q_auto | inserta f_auto,q_auto/ después de /upload/ — nunca se inyecta automáticamente |
Metadatos AEO
description: cumple una triple función: meta descripción, descripción de schema.org y la entrada de la página en/llms.txt. Escríbela para un lector que decide si hacer clic.speakable_selector: selectores CSS separados por comas que marcan las oraciones que los asistentes de voz deben leer. Predeterminado:[data-speakable], h1 + p. El camino fácil: etiqueta tus dos o tres mejores oraciones de respuesta con un atributodata-speakable; o establece un selector por página al cargar.schema_org_type: predeterminado aWebPage; valores permitidos:WebPage,Article,AboutPage,ContactPage,CollectionPage,ProfilePage. EstableceAboutPage/ContactPageen esas páginas.og_image: URLhttpsabsoluta; 1200×630 funciona en todas partes. Se usa para og:image y tarjetas de Twitter./llms.txt: las páginas personalizadas activas obtienen su propia secciónCUSTOM PAGES(y cuerpos completos resueltos por marcadores en/llms-full.txt) solo cuando el nivel de acceso del módulo de páginas personalizadas espublic.- Las entradas del sitemap, prioridad, frecuencia de cambio,
lastmod(desdeupdated_date) y los alternativos hreflang son generados por la plataforma.
Comportamiento de la caché
Las páginas personalizadas publicadas se sirven con:
Cache-Control: public, max-age=300, s-maxage=86400, stale-while-revalidate=604800Los navegadores revalidan después de cinco minutos; las cachés compartidas mantienen una copia hasta por un día y pueden servirla obsoleta hasta por una semana mientras se actualiza en segundo plano. Diseña pensando en esto: las páginas personalizadas se adaptan a contenido que tolera minutos de obsolescencia después de una republicación, no a nada en tiempo real.
CSP y seguridad
- Los proyectos pueden optar por encabezados
Content-Security-Policy, aplicados solo a URLs de páginas personalizadas, nunca a todo el sitio. La política se ensambla a partir de directivas por proyecto (default-src,script-src,style-src,img-src,connect-src,frame-src) en la configuración del sitio. - Las directivas estrictas
script-src/style-srcgobiernan tus bloques<script>y<style>en línea; una política sin'unsafe-inline'los bloquea. Planifica la página y la política juntas; las páginas autocontenidas con pocos orígenes externos mantienen la política corta y la página funcionando. - Las páginas personalizadas también envían encabezados de seguridad estándar (cumplimiento de HTTPS, protección contra sniffing de tipo de contenido).
- Los valores
hrefque usan esquemasjavascript:,data:ovbscript:(incluidas formas ofuscadas con entidades o espacios en blanco) son rechazados al cargar. Los atributossrcno se ven afectados, por lo que las imágenes en líneadata:pasan.
Ejemplo
Un esqueleto de página de un solo archivo que supera 95 en las cuatro categorías:
<style> .page { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #ffffff; color: #111827; } .hero { max-width: 64rem; margin: 0 auto; padding: 4rem 1.5rem; } .hero h1 { font-size: 3rem; line-height: 1.1; margin: 0 0 1rem; } .cta { display: inline-block; padding: 0.9rem 1.8rem; border-radius: 0.5rem; background: #14532d; color: #ffffff; text-decoration: none; } .cta:focus-visible { outline: 3px solid #14532d; outline-offset: 3px; }</style><div class="page"> <section class="hero"> <h1 data-t-key="hero.title">Coffee subscriptions, delivered weekly</h1> <p data-speakable data-t-key="hero.subtitle">Freshly roasted beans from small farms, at your door every Monday.</p> <a class="cta" href="/signup/" data-t-key="hero.cta">Start your subscription</a> <img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/hero.jpg" width="1200" height="800" alt="A bag of freshly roasted coffee beans" data-hero> <img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/tf/farm.jpg" width="800" height="600" alt="Coffee farm on a hillside at sunrise" loading="lazy"> </section></div>Por qué aprueba: CSS en línea (cero solicitudes que bloquean el renderizado), fuentes del sistema (sin descarga de fuentes), cero JavaScript, ambas imágenes dimensionadas (sin cambio de diseño) y optimizadas por Cloudinary, hero ansioso y marcado como data-hero, la imagen debajo del pliegue con carga diferida, texto alt, un botón de alto contraste con un estado de foco visible, y sin <main> propio, porque el shell ya proporciona el punto de referencia. Cada selector tiene un prefijo de clase bajo el envoltorio .page, y el envoltorio pinta un fondo explícito en lugar de confiar en el tema del shell detrás. Nunca des estilos a selectores de elementos desnudos (body, img, h1…): el cuerpo se renderiza dentro del shell de la plataforma, por lo que los selectores desnudos reestilizan el shell, no solo tu página. El title y la description que proporcionas al cargar completan la categoría SEO; el shell de la plataforma los renderiza con canonical, hreflang y sitemap gestionados.
Cargarlo devuelve "lint_findings": []. Si una publicación falla el control, el fallo nombra qué corregir:
Lighthouse pre-publish gate failed: https://www.brew.example/subscriptions scored below 95 (perf=88, a11y=96, bp=100, seo=100)Cada URL fallida lleva una lista de remediación de las auditorías específicas que fallaron. Corrígelas, vuelve a cargar y publica de nuevo.
Errores comunes
Lighthouse pre-publish gate failed: … scored below 95 (perf=…, a11y=…, bp=…, seo=…): la publicación falló; nada se publicó. Trabaja en la lista de remediación (culpables típicos: scripts ansiosos, imágenes sin tamaño o sin optimizar, hojas de estilo que bloquean el renderizado, bajo contraste), vuelve a cargar y publica de nuevo. force: true en publish-confirm anula una puntuación fallida, y la anulación se escribe en el registro de auditoría.
body_source is 2400000 bytes — above the 2 MB hard cap. Move large inline assets out of body_source (Cloudinary / static).: la carga fue rechazada directamente. Las imágenes en línea Base64 son la causa habitual: alójalas con upload-media y referencia las URLs. (Por encima de 500 KB obtienes una advertencia body-too-large primero).
Esquema href bloqueado. Un href que usa javascript:, data: o vbscript: (incluso ofuscado con entidades o espacios en blanco) rechaza la carga. Reemplázalo con una URL https real o una ruta relativa. Las URLs data: en atributos src (imágenes en línea) no se ven afectadas.
El protector de renderizado de páginas personalizadas abortó la publicación. El despliegue encontró una página personalizada renderizada a través de la canalización de markdown en lugar de la ruta opaca y se detuvo; el sitio activo sigue sirviendo la versión anterior. Esto indica un fallo de renderizado del lado de la plataforma, no un error de autoría: vuelve a ejecutar la publicación e infórmalo si persiste.
lint_findings no vacío después de una carga exitosa. No es un fallo: los hallazgos son informativos y nunca bloquean. Pero cada uno es un punto menos en Lighthouse; corrígelos antes de publicar en lugar de después de un control fallido.