Ir al contenido

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 failed y necesitas corregir las auditorías nombradas.
  • upload-custom-page devolvió lint_findings y 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, usa media="print" onload="this.media='all'" o un patrón de precarga.
  • Diferir, asíncrono o eliminar JavaScript. Cada <script> debe llevar defer (preferido) o async. 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 scripts type="module", JSON-LD y las plantillas ya no bloquean).
  • Fuentes del sistema o font-display: swap. Una pila de fuentes system-ui cuesta cero solicitudes. Si cargas una fuente personalizada, cada bloque @font-face necesita font-display: swap, o el texto será invisible mientras se carga la fuente.
  • Dimensiona cada imagen. Los atributos explícitos width y height evitan el cambio de diseño (CLS).
  • Optimiza cada imagen. Aloja a través de upload-media: devuelve URLs de Cloudinary con f_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 atributo data-hero para 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: 0 por 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> (con lang/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 alt en cada imagen (solo alt="" 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-visible visibles.

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) y description (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 https absolutas para todos los activos externos.

Reglas de lint en el momento de la carga

reglase dispara cuandosolución
body-too-largecuerpo 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/asyncañade defer
img-not-lazy<img> sin loading="lazy" y sin data-hero cercanousa carga diferida o marca el hero con data-hero
blocking-stylesheet<link rel="stylesheet"> sin media o precargainserta el CSS en línea o usa el patrón media="print"
no-font-display-swap@font-face sin font-display: swapañade font-display: swap;
cloudinary-not-autoURL de Cloudinary sin f_auto/q_autoinserta 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 atributo data-speakable; o establece un selector por página al cargar.
  • schema_org_type: predeterminado a WebPage; valores permitidos: WebPage, Article, AboutPage, ContactPage, CollectionPage, ProfilePage. Establece AboutPage/ContactPage en esas páginas.
  • og_image: URL https absoluta; 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ón CUSTOM PAGES (y cuerpos completos resueltos por marcadores en /llms-full.txt) solo cuando el nivel de acceso del módulo de páginas personalizadas es public.
  • Las entradas del sitemap, prioridad, frecuencia de cambio, lastmod (desde updated_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=604800

Los 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-src gobiernan 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 href que usan esquemas javascript:, data: o vbscript: (incluidas formas ofuscadas con entidades o espacios en blanco) son rechazados al cargar. Los atributos src no se ven afectados, por lo que las imágenes en línea data: 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.

Relacionado

Última actualización: