Saltar al contenido
Fantástico Mundo de Jon
RSS

Un sitio web desde cero a través del chat: el HTML sale listo, el CSS no

Monté un sitio web completo copiando y pegando del chat. La estructura salió casi lista y la presentación no — porque HTML tiene respuesta correcta y diseño solo tiene respuesta correcta para un contexto que el modelo no conoce.

¿Con prisa? Pide el TL;DR a Claude — lee la página y la resume.

es Traducción automática por mistral-small3.2:24b, revisada por el autor. Leer el original en portugués

La marcação de un sitio web completo sale del chat lista para pegar. La hoja de estilo no sale — y el motivo no es que el modelo sea peor en CSS que en HTML.

En las últimas semanas monté un sitio pequeño de punta a punta conversando en el navegador: portada, tres páginas internas, formulario de contacto. Sin agente, sin plugin en el editor, sin nada que escribiera archivo por mí. Yo pedía, él respondía, yo seleccionaba, Ctrl+C, Ctrl+V en VS Code, guardaba, recargaba la pestaña. El método más burro disponible hoy, y de propósito: existe Cursor, existe Copilot, existe el v0 de Vercel generando componente con preview — yo quería justamente el escenario sin herramienta sosteniendo las puntas, porque es así como la mayor parte de las personas realmente usa eso.

El resultado tiene una asimetría que no esperaba tan limpia. La estructura salió casi lista. La presentación no salió casi nada.

Cómo se hizo

Período
nov–dic de 2024
Modelos
GPT-4o y Claude 3.5 Sonnet
Método
copiar y pegar, sin agente
Editor
VS Code, sin plugin de IA
Stack
HTML + Tailwind 3
Costo
US$ 40/mes (ChatGPT Plus + Claude Pro, US$ 20 cada)

La marcação tiene plantilla; el diseño tiene contexto

Esta es la frase completa del post, y vale la pena gastar un párrafo en ella antes de los ejemplos.

Para un contenido dado, existe una marcaación correcta. Un campo de correo electrónico es input type="email" con un label asociado por for/id. Esto no depende del cliente, de la marca, del público ni de mi gusto. Es convención pública, escrita en especificación, repetida en millones de páginas — exactamente el tipo de cosa que un modelo de lenguaje aprende bien, porque la respuesta es la misma independientemente de quién pregunta.

El diseño no funciona así. ¿Cuál es el espacio correcto entre el título de la sección y el primer párrafo? No existe respuesta fuera del contexto: depende del tamaño del cuerpo de texto, del ancho de la columna, de la densidad del resto de la página, de cuánto contenido va a entrar allí realmente, de si el sitio se lee en el metro o en un monitor de 27“. Ninguna de esas informaciones está en mi prompt. El modelo no se equivoca en la respuesta — él devuelve la mediana de todas las respuestas plausibles, que es una respuesta correcta para un proyecto medio que no es el mío.

Es la diferencia entre una pregunta de prueba y una pregunta de proyecto. El chat es excelente en la primera y estructuralmente limitado en la segunda.

Lo que sale listo sale listo de verdad

Siendo justo con la parte que funciona, porque ella es la mayor parte del volumen de código.

Pedí el formulario de contacto en una frase — nombre, correo electrónico, teléfono, mensaje — y volvió esto:

<form action="/contato" method="post" novalidate>
  <div>
    <label for="nome">Nome</label>
    <input id="nome" name="nome" type="text" autocomplete="name" required />
  </div>
  <div>
    <label for="email">E-mail</label>
    <input id="email" name="email" type="email"
           autocomplete="email" inputmode="email" required />
  </div>
  <div>
    <label for="telefone">Telefone</label>
    <input id="telefone" name="telefone" type="tel"
           autocomplete="tel" inputmode="tel" />
  </div>
  <div>
    <label for="mensagem">Mensagem</label>
    <textarea id="mensagem" name="mensagem" rows="6" required></textarea>
  </div>
  <button type="submit">Enviar mensagem</button>
</form>

Mira lo que está ahí sin yo haberlo pedido: autocomplete correcto en cada campo, inputmode para teclado de celular, type="tel" en vez de text, label amarrado por for, required solo en los campos que tienen sentido, el botón con type="submit" explícito. Esto es mejor que muchos formularios que ya he revisado en producción escritos por personas.

Lo mismo vale para la componentización obvia. Pide un encabezado, un pie de página y una tarjeta de servicio y el corte entre ellos viene en el lugar que cualquier dev haría. Y el texto de relleno es decente: en vez de lorem ipsum, viene “Atendimiento en hasta 24 horas útiles” y “Presupuesto sin compromiso”, que es malo como copy final y excelente como andamio — da para diagramar encima y ver el diseño con texto del tamaño real, en portugués, con acento, que es donde el lorem ipsum engaña.

Nada de esto es poco. Es la parte aburrida del trabajo, hecha en segundos, con una tasa de acierto alta.

La marcaación está correcta y la semántica está en la media

Ahora la primera grieta, que es sutil porque no parece grieta: el HTML válido no es lo mismo que HTML bien estructurado.

Lo que vino para la portada:

<div class="header">
  <div class="logo">Ateliê Marena</div>
  <div class="menu">
    <div class="menu-item"><a href="/servicos">Serviços</a></div>
    <div class="menu-item"><a href="/contato">Contato</a></div>
  </div>
</div>

<div class="hero">
  <div class="hero-title">Móveis sob medida</div>
  <div class="hero-text">Projeto, marcenaria e instalação.</div>
</div>

<div class="services">
  <div class="service-card">
    <div class="service-title">Projeto 3D</div>
    <div class="service-desc">Você vê antes de aprovar.</div>
  </div>
</div>

Se renderiza perfectamente. Pasa en el validador. Y es una página sin ningún punto de referencia para quien no ve la pantalla: ningún landmark, ningún encabezado de nivel, nada que un lector de pantalla pueda usar para saltar directamente al contenido o listar las secciones de la página.

Lo que tuve que escribir a mano:

<header>
  <a href="/" class="logo">Ateliê Marena</a>
  <nav aria-label="Principal">
    <ul>
      <li><a href="/servicos">Serviços</a></li>
      <li><a href="/contato">Contato</a></li>
    </ul>
  </nav>
</header>

<main>
  <section aria-labelledby="titulo-hero">
    <h1 id="titulo-hero">Móveis sob medida</h1>
    <p>Projeto, marcenaria e instalação.</p>
  </section>

  <section aria-labelledby="titulo-servicos">
    <h2 id="titulo-servicos">Serviços</h2>
    <article>
      <h3>Projeto 3D</h3>
      <p>Você vê antes de aprovar.</p>
    </article>
  </section>
</main>

El veredicto es el mismo en todos los casos que vi: el modelo elige div cuando el elemento correcto existe, porque div es la respuesta que nunca está equivocada. No es ignorancia — si pregunto “¿qué elemento debo usar para la navegación?”, él responde nav sin dudar y explica perfectamente por qué. Simplemente no elige el elemento correcto cuando nadie exige, del mismo modo que nadie exigió en la mayor parte del HTML de internet en el que fue entrenado.

El contraste reproba sin que nadie note

Este es mi favorito, porque es el error que sobrevive a todas las etapas de revisión que una persona común hace.

Pedí “un botón destacado, con el color principal del sitio”. Volvió:

<button class="bg-indigo-500 text-white px-6 py-3 rounded-lg">
  Solicitar orçamento
</button>

<p class="text-gray-400 mt-2">Resposta em até 24 horas úteis.</p>

Queda bonito. Queda exactamente con la cara de lo que imaginaste al leer el párrafo. Y las dos líneas reproban en el criterio de contraste del WCAG.

Blanco sobre #6366f1 — el indigo-500 del Tailwind 3 — da 4,47:1. El mínimo para texto normal en AA es 4,5:1. Reproba por tres centésimos, un margen que ningún ojo humano detecta y ningún navegador avisa. El text-gray-400 es peor y más común: #9ca3af sobre blanco da 2,54:1, casi la mitad del exigido, y es la clase estándar de “texto secundario” en prácticamente todo componente que sale del chat.

Par de colores Contraste (n:1) AA texto normal
#ffffff sobre #6366f1 4,47 reprova
#9ca3af sobre #ffffff 2,54 reprova
#6b7280 sobre #ffffff 4,83 passa
#ffffff sobre #4f46e5 6,29 passa
Cálculo por la fórmula de luminancia relativa de la WCAG 2.x, no medición — dos hexadecimales bastan. Las dos primeras líneas son lo que el chat devolvió; las dos últimas son la corrección, un escalón en la escala del Tailwind. AA pide 4,5:1 para texto normal y 3:1 para texto grande.

La cuenta cabe en diez líneas y no depende de ninguna biblioteca:

const canal = (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));

const luminancia = (hex) => {
  const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
  return 0.2126 * canal(r) + 0.7152 * canal(g) + 0.0722 * canal(b);
};

const contraste = (a, b) => {
  const [x, y] = [luminancia(a), luminancia(b)].sort((m, n) => n - m);
  return (x + 0.05) / (y + 0.05);
};
node contraste.js

#ffffff sobre #6366f1 4.47:1 reprova #9ca3af sobre #ffffff 2.54:1 reprova #6b7280 sobre #ffffff 4.83:1 passa #ffffff sobre #4f46e5 6.29:1 passa

los cuatro pares de la tabla arriba

La corrección es ridículamente barata: indigo-500 se convierte en indigo-600 y el blanco pasa a tener 6,29:1; gray-400 se convierte en gray-500 y el secundario sube a 4,83:1. Un dígito en el hexadecimal separa reprobar de pasar. El problema nunca fue la dificultad de la corrección — fue nadie saber que había algo para corregir.

La clase que no existe no da error

En algún momento apareció esto en mi HTML:

<div class="flex-center gap-4 text-md shadow-soft hover:scale-102">

Cuatro de esas seis clases no existen en el Tailwind 3. gap-4 existe. hover: es prefijo válido. flex-center, text-md, shadow-soft y scale-102 son invenciones — utilitarios plausibles, con el nombre que deberían tener, que cualquier persona juraría hacer parte del framework.

El detalle cruel: text-md no existe porque la escala del Tailwind es text-sm, text-base, text-lg. scale-102 no existe porque la escala salta de scale-100 a scale-105. flex-center es la fusión obvia de flex items-center justify-center, que todos ya quisieron que existiera. Son exactamente los nombres que un humano adivinaría.

Lo mismo ocurrió con Bootstrap 5, cuando probé la misma página en el otro framework:

<div class="d-flex-center mt-6 text-medium">

mt-6 no existe: la escala de espaciado del Bootstrap 5 va de 0 a 5. d-flex-center y text-medium tampoco. Y — este es el punto — nada de esto emite un error. No hay aviso en la consola, no hay subrayado rojo en el editor, el build no falla. La clase simplemente no genera ninguna regla. El elemento queda sin estilo, tú miras, piensas que el espaciado “quedó medio apretado”, y corriges el síntoma en el elemento de al lado.

Encontré todas las clases inventadas del proyecto de una vez, y ninguna de ellas mirando la pantalla. Las descubrí todas a la vez, mirando el CSS generado y buscando lo que no tenía salida.

Todo hover sin focus borra la navegación por teclado

Este error es 100% consistente. Cada vez que pedí un estado de interacción, vino solo el del ratón:

.botao {
  background: #4f46e5;
  color: #ffffff;
  transition: background 150ms;
}

.botao:hover {
  background: #4338ca;
}

Funciona bien para quien usa ratón y no existe para quien usa teclado. Peor: si en algún momento el CSS lleva un outline: none — y lo lleva, porque “quitar ese borde azul feo del Chrome” es una solicitud común y el modelo atiende sin discutir —, la persona que navega por Tab pierde completamente la noción de dónde está en la página.

Lo que el bloque necesita tener:

.botao:hover {
  background: #4338ca;
}

/* o par que nunca vem junto */
.botao:focus-visible {
  outline: 2px solid #b23c17;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .botao { transition: none; }
}

focus-visible en vez de focus para no encender el anillo al clic del ratón; outline-offset para que el anillo no muera contra el relleno del botón; y la regla de movimiento reducido, que tampoco nunca viene sola. Ninguna de estas tres líneas es difícil. Las tres dependen de alguien recordar de pedir.

El diseño funciona en el monitor que el modelo imaginó

El clásico, y lo que más consumió rondas de chat. El hero que volvió:

.hero {
  height: 100vh;
  display: grid;
  place-items: center;
}

.servicos {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px;
  max-width: 1200px;
}

.depoimento {
  position: absolute;
  top: 420px;
  right: 80px;
  width: 380px;
}

Tres trampas en doce líneas, todas del mismo tipo: cada valor fue elegido para un viewport específico que nadie declaró.

height: 100vh en el celular cuenta la barra de dirección del navegador, entonces el contenido es cortado o salta cuando la barra se recoge — y si el texto del hero es mayor que lo previsto, él desborda fuera del bloque en vez de empujar la página. repeat(4, 1fr) sin media query se convierte en cuatro columnas de 70px en un teléfono. Y el position: absolute con top: 420px es el peor de los tres, porque funciona perfectamente en el ancho en que el modelo pensó y se desmonta en cualquier otro, sin nunca dar señal de que algo está equivocado en el código.

La versión que resuelve los tres no es más complicada, solo es escrita sin suponer la pantalla:

.hero {
  min-height: 100svh;      /* svh respeita a barra do navegador móvel */
  display: grid;
  place-items: center;
}

.servicos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.5rem;
}

min-height en vez de height deja que el contenido crezca. auto-fit con minmax refluye solo y dispensa breakpoint. Y el testimonio se convierte en un ítem del flujo, no una pieza flotando en una coordenada.

Nada de esto es conocimiento oscuro, y el modelo conoce todo: pide explícitamente “grid responsivo sin media query” y él devuelve auto-fit al instante. Él solo no elige por sí mismo, porque repeat(4, 1fr) es lo que aparece en la mayoría de los ejemplos de tutorial — y tutorial se escribe para una pantalla de captura de pantalla.

Pedir “hazlo bonito” devuelve siempre el mismo sitio

Aquí está la parte que yo creo realmente útil de este post, y la que cambió mi forma de trabajar.

Pedí “hazlo más bonito y moderno” en varios puntos del proyecto. Recibí el mismo sitio todas las veces. No parecido: el mismo. Gradiente morado-para-azul en el hero, tarjeta blanca con border-radius generoso y sombra difusa, Inter como fuente, ícono redondeado encima de cada bloque de servicio, una fila de tres tarjetas idénticas, y un pie de página oscuro. Si ya has visto cualquier landing page de producto de los últimos tres años, ya has visto exactamente esa página.

Y esto no es pereza del modelo. Es el comportamiento correcto de él.

Un modelo de lenguaje devuelve la continuación más probable. “Bonito” y “moderno” son adjetivos sin referente — no apuntan a ningún valor, ningún color, ninguna medida. Ante una solicitud sin restricción, el único camino es la media del material de entrenamiento, y la media del material de entrenamiento de “sitio bonito y moderno” es literalmente aquello: el dialecto visual que dominó el Dribbble y los templates de 2021 a 2023. Pedir estética sin restricción es pedir la moda dominante. Vas a recibir la moda dominante, con precisión, siempre.

La salida no es escribir un adjetivo mejor. No existe adjetivo mejor. La salida es dejar de mandar adjetivo.

Compara los dos pedidos. Primero, el que yo mandaba:

Deixa a página mais bonita e moderna, com uma cara mais profissional.

Ahora lo que pasé a mandar — mismo modelo, misma sesión, mismo HTML de entrada:

Estilize com estas restrições. Não introduza nenhum valor fora delas.

Cores (só estas):
  fundo       #ffffff
  superfície  #f4f6f4
  texto       #1c211d
  secundário  #5f6b62
  acento      #1f5d3a   -> links, foco e botão primário
  marcação    #b23c17   -> só alerta, no máximo 2% da tela
  régua       #d9e0da   -> 1px, único separador do sistema

Tipografia:
  títulos  Archivo 700, entrelinha 1.06, tracking -0.03em
  corpo    Source Serif 4 400, 19px, entrelinha 1.72, coluna de 68ch
  mono     JetBrains Mono 13px, apenas em código e números

Espaçamento: use só 4, 8, 16, 24, 40, 64 e 104px. Nenhum valor intermediário.
Raio: 0, 2px ou 4px. Nada acima.
Sombra: nenhuma. Profundidade é régua de 1px ou superfície um degrau abaixo.
Estados: todo :hover tem :focus-visible equivalente, anel sólido de 2px.
Contraste: corpo no mínimo 7:1, secundário no mínimo 4,5:1.
Larguras: nenhum container com px fixo. Testar em 320, 768 e 1440.

Se algo do meu HTML não couber nessas regras, me diga em vez de inventar valor.

El segundo devuelve un sitio que es mío. No porque el modelo se volvió más creativo — se volvió menos, y eso es lo que resuelve. Con el espacio de respuesta cerrado, él deja de elegir entre un millón de páginas medias y pasa a hacer la única cosa que hace muy bien: aplicar un conjunto de reglas a un conjunto de elementos, con consistencia sobrehumana. Él no olvida un espaciado en veinte archivos. Yo sí.

El intercambio es claro: estética no es delegable, aplicación de estética sí. Alguien necesita decidir la paleta, la escala y las reglas — eso es trabajo de proyecto, y no sale de un prompt de una línea. Después de decidido, el chat es el mejor ejecutor barato que ha existido para eso.

Y la última línea del bloque vale por la mitad de él. “Dime en vez de inventar valor” cambia una decisión silenciosa — que es cara, porque solo descubres después — por una pregunta, que es barata.

Accesibilidad es el agujero que no hace ruido

Junta lo que apareció hasta aquí y repare en el patrón: div en lugar de nav, contraste de 4,47:1, hover sin focus, outline: none, height: 100vh. Todos son fallos de accesibilidad. Y ninguno de ellos se manifiesta.

Error de sintaxis rompe el build. Error de lógica rompe la prueba. Error de accesibilidad no hace nada: la página se abre, se renderiza, queda bonita en la captura de pantalla, el cliente aprueba, el sitio va al aire. El feedback solo llega por un camino que la mayoría de los proyectos nunca recorre — alguien navegando por teclado, alguien usando lector de pantalla, alguien con baja visión en el celular bajo sol, o una auditoría.

Fue lo que hice al final, y es el paso que yo recomendaría a cualquier persona que monte un sitio de esta manera. Una pasada de Lighthouse y otra de axe DevTools, las dos gratuitas y dentro del navegador, encontraron en pocos minutos una lista de problemas que no había visto en semanas mirando la página. Después de eso, tres pruebas manuales que cuestan dos minutos cada una y capturan lo que herramienta automática no captura:

  • recorrer la página entera solo con Tab y verificar si da para ver dónde está el foco, todo el tiempo;
  • dar zoom de 200% y conferir si algún texto desapareció o apareció scroll horizontal;
  • estrechar la ventana hasta 320px de ancho y ver qué se rompe.

Ninguno de estos tres necesita conocimiento especializado. Necesitan solo a alguien que recuerde hacer — y es exactamente ese “recordar” lo que el chat no hace por ti, porque él responde a lo que fue preguntado y nadie preguntó.

Dónde esto vale igualmente

Terminé con un sitio funcionando, y no me arrepiento del método. Solo sé mejor dónde se aplica.

Vale mucho para prototipo. Cuando el objetivo es ver la idea en pantalla para decidir si ella sobrevive, el sitio medio es excelente — él es medio justamente por ser familiar, y familiar es lo que quieres en una demostración. Nada de eso va a producción, entonces deuda de accesibilidad ni nace.

Vale para landing descartable. Página de un evento que muere en tres semanas, formulario de inscripción de una clase. Costo de mantenimiento cero, porque no hay mantenimiento.

Y vale, con una reserva seria, para quien no es del front y necesita algo que funcione. Un back-end que necesita un panel interno, alguien montando el sitio de su propio negocio. Aquí el chat entrega, en una tarde, algo que esa persona no construiría en una semana. La reserva es que ella tampoco tiene cómo ver ninguno de los problemas de este post — y por eso la auditoría automática deja de ser buena práctica y se vuelve obligatoria. Dos herramientas, diez minutos, gratis.

Donde yo no usaría así: cualquier cosa con marca propia a defender, cualquier cosa que otra persona va a mantener, cualquier cosa en que accesibilidad sea requisito contractual. No porque el modelo no dé cuenta — porque en esos casos el trabajo difícil es decidir las restricciones, y esa parte sigue siendo mía.

Lo que quedó, al final, es una regla de división de trabajo que yo no tenía antes. El modelo es excelente donde existe una respuesta correcta y pésimo donde solo existe una respuesta adecuada — y la diferencia entre las dos no es dificultad técnica, es la presencia de un contexto que solo yo tengo. HTML tiene plantilla. CSS tiene proyecto. Mientras yo mande adjetivo, recibo la media de internet; cuando mando hexadecimal, escala y regla, recibo mi sitio — montado por una máquina que no se equivoca en la vigésima aplicación de la misma regla, que es precisamente donde yo me equivoco.