Tema
Proyecto: landing de cafetería
Es momento de juntar todo lo que aprendiste. En este proyecto vas a construir, paso a paso, la landing page completa de un negocio real: "Café Aroma", una cafetería de especialidad. Es exactamente el tipo de sitio que un pequeño negocio te pedirá como primer proyecto: presentación, menú, historia, opiniones, ubicación y formulario de reservas.
Objetivo del proyecto
Al terminar tendrás una página que:
- Usa HTML semántico de principio a fin.
- Es accesible: skip link, landmarks, labels, foco visible,
alt. - Está lista para SEO: título, descripción, Open Graph y datos estructurados.
- Incluye un formulario de reserva con validación nativa.
- Es responsive con unas cuantas líneas de CSS (sin frameworks).
Este es el resultado final. Pruébalo en los modos móvil, tablet y escritorio, y navega con Tab:
Paso 0: planear antes de escribir
Antes de abrir el editor, un profesional define con el cliente:
- Objetivo de la página: que la gente reserve mesa o visite el local.
- Llamada a la acción principal (CTA): "Reserva tu mesa".
- Secciones: hero, menú, nosotros, opiniones, contacto/reserva.
- Contenido real: textos, precios, fotos, horario, dirección, teléfono, redes.
Con eso hacemos el esqueleto semántico:
txt
body
├── a.saltar (skip link)
├── header (logo + nav)
├── main#contenido
│ ├── section.hero (h1 + CTA)
│ ├── section#menu (h2 + 3 article)
│ ├── section#nosotros (h2 + figure + texto)
│ ├── section#opiniones (h2 + blockquote)
│ └── section#contacto (h2 + address + dl + form)
└── footerEstructura de archivos para el proyecto real:
txt
cafe-aroma/
├── index.html
├── css/estilos.css
├── js/app.js
└── img/
├── hero.webp
├── barista.webp
└── og-cafe-aroma.jpgSobre la demo
Para que la demo funcione en un solo archivo, el CSS va dentro del head y las "fotos" son degradados con emojis. En tu proyecto real usa archivos separados e imágenes reales optimizadas, como aprendiste en Imágenes.
Paso 1: el head
html
<!DOCTYPE html>
<html lang="es-MX">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO: título con servicio + ubicación + marca -->
<title>Café Aroma | Cafetería de especialidad en Coyoacán</title>
<meta name="description" content="Café de especialidad, pan artesanal y un espacio acogedor en Coyoacán. Consulta nuestro menú, horarios y reserva tu mesa.">
<link rel="canonical" href="https://cafearoma.mx/">
<!-- Vista previa al compartir en WhatsApp/Facebook -->
<meta property="og:type" content="website">
<meta property="og:title" content="Café Aroma | Cafetería en Coyoacán">
<meta property="og:description" content="El mejor café de olla de la ciudad. ¡Reserva tu mesa!">
<meta property="og:image" content="https://cafearoma.mx/img/og-cafe-aroma.jpg">
<meta property="og:url" content="https://cafearoma.mx/">
<meta name="twitter:card" content="summary_large_image">
<!-- Iconos -->
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta name="theme-color" content="#3e2723">
<link rel="stylesheet" href="css/estilos.css">
<script src="js/app.js" defer></script>
</head>Después agregarás el JSON-LD de negocio local (lo verás en el paso 8).
Paso 2: skip link y encabezado
html
<body>
<!-- Primer elemento enfocable: permite saltar el menú con el teclado -->
<a class="saltar" href="#contenido">Saltar al contenido</a>
<header class="encabezado">
<div class="contenedor">
<!-- El logo enlaza al inicio -->
<a class="logo" href="/">☕ Café Aroma</a>
<!-- Navegación principal con anclas a cada sección -->
<nav aria-label="Principal">
<ul class="menu">
<li><a href="#menu">Menú</a></li>
<li><a href="#nosotros">Nosotros</a></li>
<li><a href="#opiniones">Opiniones</a></li>
<li><a href="#contacto">Contacto</a></li>
</ul>
</nav>
</div>
</header>Decisiones clave:
- El menú es una lista dentro de
<nav>conaria-label. - En el sitio real el logo sería un
<img src="img/logo.svg" alt="Café Aroma - Inicio">. - El
<div class="contenedor">no tiene significado: solo sirve para centrar el contenido con CSS. Ese es un uso correcto de<div>.
Paso 3: el hero
La primera pantalla debe responder en 3 segundos: qué es, dónde está y qué hago ahora.
html
<main id="contenido">
<section class="hero" aria-labelledby="titulo-hero">
<div class="contenedor">
<!-- El único h1 de la página: describe el negocio -->
<h1 id="titulo-hero">Café de especialidad en el corazón de Coyoacán</h1>
<p>Granos mexicanos tostados cada semana, pan recién horneado y un lugar para quedarte.</p>
<!-- CTA: lleva a la sección de reservas -->
<a class="boton" href="#contacto">Reserva tu mesa</a>
</div>
</section>Fíjate en que la CTA es un enlace (lleva a otra sección), no un botón. Con una foto real de fondo usarías una imagen con fetchpriority="high" o un background-image en CSS con buen contraste para el texto.
Paso 4: el menú con tarjetas
Cada producto se entiende por sí solo, así que es un <article>:
html
<section id="menu" aria-labelledby="titulo-menu">
<div class="contenedor">
<h2 id="titulo-menu">Nuestro menú</h2>
<p class="intro">Los favoritos de nuestros clientes, preparados al momento.</p>
<div class="tarjetas">
<article class="tarjeta">
<!-- Emoji decorativo: oculto para lectores de pantalla -->
<span class="icono" aria-hidden="true">☕</span>
<h3>Café de olla</h3>
<p>Con canela y piloncillo, receta de la abuela.</p>
<p class="precio">$45</p>
</article>
<article class="tarjeta">
<span class="icono" aria-hidden="true">🥛</span>
<h3>Latte de la casa</h3>
<p>Espresso doble de Chiapas con leche texturizada.</p>
<p class="precio">$55</p>
</article>
<article class="tarjeta">
<span class="icono" aria-hidden="true">🥐</span>
<h3>Croissant de mantequilla</h3>
<p>Horneado cada mañana con mantequilla francesa.</p>
<p class="precio">$38</p>
</article>
</div>
</div>
</section>La jerarquía queda: <h1> (negocio) → <h2> (Nuestro menú) → <h3> (cada producto). En el sitio real, cambia el emoji por una foto:
html
<img src="img/cafe-de-olla.webp" alt="Jarrito de barro con café de olla y una raja de canela"
width="600" height="400" loading="lazy">Paso 5: nuestra historia
html
<section id="nosotros" class="nosotros" aria-labelledby="titulo-nosotros">
<div class="contenedor">
<!-- En la demo es un degradado; en el sitio real, una img dentro del figure -->
<figure class="foto" role="img" aria-label="Barista preparando café en nuestra barra">☕</figure>
<div>
<h2 id="titulo-nosotros">Nuestra historia</h2>
<p>Abrimos en <time datetime="2018-03">marzo de 2018</time> con una idea sencilla:
servir el mejor café mexicano, comprado directamente a productores de Veracruz,
Chiapas y Oaxaca.</p>
<p>Hoy somos un punto de encuentro para vecinos, estudiantes y quienes trabajan a distancia.</p>
</div>
</div>
</section>Versión con foto real:
html
<figure>
<img src="img/barista.webp" alt="Barista preparando café en nuestra barra"
width="800" height="600" loading="lazy">
<figcaption>Nuestro barista principal, Luis.</figcaption>
</figure>Paso 6: opiniones
Los testimonios generan confianza. Son citas, así que usamos <blockquote>:
html
<section id="opiniones" aria-labelledby="titulo-opiniones">
<div class="contenedor">
<h2 id="titulo-opiniones">Lo que dicen nuestros clientes</h2>
<p class="intro">Más de 500 reseñas con 4.8 estrellas.</p>
<div class="tarjetas">
<blockquote>
<p>“El mejor café de olla que he probado en la ciudad.”</p>
<cite>— Mariana G.</cite>
</blockquote>
<blockquote>
<p>“Wifi rápido, enchufes en cada mesa y un latte delicioso.”</p>
<cite>— Jorge R.</cite>
</blockquote>
</div>
</div>
</section>Buena práctica
Usa opiniones reales del cliente (por ejemplo, de su perfil de Google) y pide permiso para publicarlas. Inventar reseñas es engañoso y puede tener consecuencias legales.
Paso 7: contacto, horario y reserva
html
<section id="contacto" class="contacto" aria-labelledby="titulo-contacto">
<div class="contenedor">
<div>
<h2 id="titulo-contacto">Visítanos</h2>
<!-- address: datos de contacto del negocio -->
<address>
Av. Francisco Sosa 123, Coyoacán, CDMX<br>
Tel: <a href="tel:+525512345678">55 1234 5678</a><br>
Correo: <a href="mailto:hola@cafearoma.mx">hola@cafearoma.mx</a>
</address>
<!-- Horario como lista de descripción -->
<h3>Horario</h3>
<dl>
<dt>Lun – Vie</dt><dd>7:00 – 21:00</dd>
<dt>Sáb – Dom</dt><dd>8:00 – 22:00</dd>
</dl>
</div>
<!-- Formulario de reserva con validación nativa -->
<form id="reserva" action="https://formspree.io/f/TU_ID" method="post">
<h3>Reserva una mesa</h3>
<div>
<label for="nombre">Nombre</label>
<input id="nombre" name="nombre" type="text" required autocomplete="name">
</div>
<div>
<label for="tel">Teléfono</label>
<input id="tel" name="telefono" type="tel" required pattern="[0-9]{10}"
inputmode="numeric" autocomplete="tel" placeholder="10 dígitos">
</div>
<div>
<label for="personas">Personas</label>
<select id="personas" name="personas" required>
<option value="">Selecciona</option>
<option>1–2</option>
<option>3–4</option>
<option>5 o más</option>
</select>
</div>
<div>
<label for="fecha">Fecha y hora</label>
<input id="fecha" name="fecha" type="datetime-local" required>
</div>
<button class="boton" type="submit">Enviar reserva</button>
<!-- Región viva: anuncia el mensaje de éxito a lectores de pantalla -->
<p id="aviso" role="status"></p>
</form>
</div>
</section>
</main>Para que el formulario envíe correos sin programar un servidor, puedes usar un servicio como Formspree o Web3Forms: creas una cuenta, te dan una URL y la pones en action. Si después aprendes backend (PHP, Node), apuntarás a tu propio script.
Paso 8: pie de página y datos estructurados
html
<footer class="pie">
<div class="contenedor">
<p>© 2026 Café Aroma ·
<a href="https://instagram.com/cafearoma" target="_blank" rel="noopener noreferrer">Instagram</a> ·
<a href="/aviso-de-privacidad">Aviso de privacidad</a>
</p>
</div>
</footer>
</body>
</html>Y en el <head>, el JSON-LD para que Google muestre horario y dirección:
html
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "CafeOrCoffeeShop",
"name": "Café Aroma",
"url": "https://cafearoma.mx/",
"telephone": "+52-55-1234-5678",
"priceRange": "$",
"address": {
"@type": "PostalAddress",
"streetAddress": "Av. Francisco Sosa 123",
"addressLocality": "Coyoacán",
"addressRegion": "CDMX",
"addressCountry": "MX"
},
"openingHours": ["Mo-Fr 07:00-21:00", "Sa-Su 08:00-22:00"]
}
</script>Paso 9: el CSS básico
Este curso es de HTML, así que el CSS es intencionalmente sencillo. Lo importante: variables de color, un contenedor centrado, Grid con auto-fit para que las tarjetas se acomoden solas, y estados de foco visibles. Lo entenderás a fondo en el curso de CSS.
Ver el CSS completo (css/estilos.css)
css
:root { --cafe: #3e2723; --crema: #fdf6ec; --acento: #c8763a; --texto: #2b1d16; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--texto); background: var(--crema); line-height: 1.6; }
img { max-width: 100%; height: auto; display: block; }
a { color: var(--acento); }
.contenedor { width: min(1100px, 100% - 32px); margin-inline: auto; }
/* Skip link: oculto hasta recibir foco */
.saltar { position: absolute; left: 8px; top: -60px; background: var(--cafe); color: #fff; padding: 8px 14px; border-radius: 6px; z-index: 10; }
.saltar:focus { top: 8px; }
/* Encabezado fijo */
.encabezado { background: var(--cafe); color: #fff; position: sticky; top: 0; z-index: 5; }
.encabezado .contenedor { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-block: 12px; flex-wrap: wrap; }
.logo { color: #fff; font-weight: 800; font-size: 1.25rem; text-decoration: none; }
.menu { list-style: none; display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0; padding: 0; }
.menu a { color: #f5deb3; text-decoration: none; font-weight: 500; }
.menu a:hover, .menu a:focus-visible { color: #fff; text-decoration: underline; }
/* Hero */
.hero { background: linear-gradient(rgb(62 39 35 / .75), rgb(62 39 35 / .85)), radial-gradient(circle at 30% 30%, #8d6e63, #3e2723); color: #fff; text-align: center; padding: 72px 0; }
.hero h1 { font-size: clamp(1.8rem, 5vw, 3rem); line-height: 1.15; margin: 0 0 12px; }
.hero p { font-size: 1.1rem; margin: 0 auto 24px; max-width: 520px; color: #f5deb3; }
.boton { display: inline-block; background: var(--acento); color: #fff; padding: 12px 26px; border-radius: 999px; text-decoration: none; border: 0; font: inherit; font-weight: 700; cursor: pointer; }
.boton:hover { background: #a85f2a; }
/* Secciones y tarjetas */
section { padding: 56px 0; }
section h2 { text-align: center; font-size: 1.8rem; margin: 0 0 8px; color: var(--cafe); }
.intro { text-align: center; max-width: 560px; margin: 0 auto 32px; }
.tarjetas { display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.tarjeta { background: #fff; border-radius: 14px; padding: 22px; box-shadow: 0 4px 18px rgb(62 39 35 / .08); }
.tarjeta .icono { font-size: 2rem; }
.tarjeta h3 { margin: 8px 0 4px; }
.precio { font-weight: 800; color: var(--acento); font-size: 1.1rem; }
.nosotros { background: #fff; }
.nosotros .contenedor { display: grid; gap: 28px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); align-items: center; }
.nosotros h2 { text-align: left; }
.foto { aspect-ratio: 4 / 3; border-radius: 14px; background: linear-gradient(135deg, #a1887f, #4e342e); display: grid; place-items: center; font-size: 4rem; margin: 0; }
blockquote { margin: 0; background: #fff; border-left: 4px solid var(--acento); padding: 18px; border-radius: 10px; }
blockquote p { margin: 0 0 8px; font-style: italic; }
cite { font-style: normal; font-weight: 700; }
/* Contacto y formulario */
.contacto .contenedor { display: grid; gap: 28px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.contacto h2 { text-align: left; }
form { display: grid; gap: 12px; }
label { font-weight: 600; display: block; margin-bottom: 4px; }
input, select, textarea { width: 100%; padding: 10px 12px; font: inherit; border: 2px solid #d7ccc8; border-radius: 8px; background: #fff; }
input:focus, select:focus, textarea:focus { outline: 3px solid rgb(200 118 58 / .4); border-color: var(--acento); }
input:user-invalid, textarea:user-invalid { border-color: #c62828; }
address { font-style: normal; }
dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; }
dt { font-weight: 700; }
dd { margin: 0; }
.pie { background: var(--cafe); color: #d7ccc8; text-align: center; padding: 24px 0; font-size: .9rem; }
.pie a { color: #f5deb3; }Detalles del CSS que vale la pena notar:
repeat(auto-fit, minmax(220px, 1fr)): las tarjetas pasan de 3 columnas a 1 sin media queries.width: min(1100px, 100% - 32px): contenedor centrado con margen lateral de 16px en celular.position: stickymantiene el encabezado visible al hacer scroll.:user-invalidmarca en rojo los campos inválidos solo después de que el usuario los tocó.
Paso 10: un toque de JavaScript (opcional)
En la demo, el envío se simula para mostrar un mensaje. En producción, el action del formulario hace el trabajo; este script es solo para mostrar la confirmación sin salir de la página:
js
// js/app.js
document.getElementById('reserva').addEventListener('submit', (e) => {
e.preventDefault(); // en producción, aquí enviarías los datos con fetch()
document.getElementById('aviso').textContent = '¡Gracias! Te confirmaremos por teléfono.';
e.target.reset();
});Aprenderás a enviar formularios con fetch() en el curso de JavaScript.
Paso 11: revisión final
Antes de dar el proyecto por terminado, aplica el checklist de Buenas prácticas:
- Validador W3C: cero errores.
- Teclado: Tab muestra el skip link, recorre el menú, la CTA y todo el formulario con foco visible.
- Lighthouse en modo móvil: 90+ en Rendimiento y 95+ en Accesibilidad, Buenas prácticas y SEO.
- Celular real: el teléfono abre la app de llamadas, el formulario muestra el teclado numérico en "Teléfono" y el calendario nativo en "Fecha".
- Compartir: pega la URL en WhatsApp y revisa la vista previa.
Error común
Olvidar actualizar el año del ©, dejar el TU_ID de Formspree sin reemplazar o no probar que el correo de reserva realmente llegue al cliente. Haz siempre una reserva de prueba de punta a punta.
Ejercicios
- Agrega una sección "Preguntas frecuentes" con tres
<details>exclusivos entre "Opiniones" y "Contacto", y añádela al menú. - Agrega un mapa responsivo de Google Maps (o OpenStreetMap) en la sección de contacto, con
titleyloading="lazy". - Agrega un botón flotante de WhatsApp accesible en la esquina inferior derecha.
- Reto: adapta la landing para otro negocio (una barbería, un consultorio o un estudio de yoga) cambiando contenido, colores y el tipo de JSON-LD.
Ver solución
html
<!-- Ejercicio 1: en el nav agrega <li><a href="#faq">Preguntas</a></li> -->
<section id="faq" aria-labelledby="titulo-faq">
<div class="contenedor">
<h2 id="titulo-faq">Preguntas frecuentes</h2>
<details name="faq">
<summary>¿Tienen leche vegetal?</summary>
<p>Sí: almendra, avena y coco sin costo extra.</p>
</details>
<details name="faq">
<summary>¿Aceptan mascotas?</summary>
<p>Sí, en la terraza. Tenemos agua y premios para ellas.</p>
</details>
<details name="faq">
<summary>¿Puedo trabajar desde la cafetería?</summary>
<p>Claro, hay wifi gratuito y enchufes en todas las mesas.</p>
</details>
</div>
</section>
<!-- Ejercicio 2: dentro de la sección de contacto -->
<iframe class="mapa"
src="https://www.google.com/maps/embed?pb=CODIGO_DE_GOOGLE"
title="Mapa de ubicación de Café Aroma en Coyoacán"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"></iframe>
<!-- Ejercicio 3: antes de cerrar body -->
<a class="whatsapp" href="https://wa.me/525512345678?text=Hola,%20quiero%20reservar"
target="_blank" rel="noopener noreferrer" aria-label="Escríbenos por WhatsApp">
<span aria-hidden="true">💬</span>
</a>css
.mapa { width: 100%; aspect-ratio: 4 / 3; border: 0; border-radius: 14px; }
.whatsapp {
position: fixed; right: 16px; bottom: 16px; z-index: 20;
width: 56px; height: 56px; border-radius: 50%;
display: grid; place-items: center;
background: #25d366; font-size: 1.6rem; text-decoration: none;
box-shadow: 0 6px 20px rgb(0 0 0 / .25);
}
.whatsapp:focus-visible { outline: 3px solid #3e2723; outline-offset: 3px; }Resumen
- Planea objetivo, CTA, secciones y contenido real antes de escribir código.
- Estructura semántica:
<header>+<nav>,<main>con una<section>por bloque,<footer>. - Un
<h1>en el hero,<h2>por sección y<h3>en tarjetas. <article>para productos,<blockquote>para opiniones,<address>y<dl>para contacto y horario.- Formulario con labels,
autocomplete,inputmodey validación nativa. - Head completo con SEO, Open Graph, iconos y JSON-LD.
- Revisa con el validador, el teclado, Lighthouse y un celular real antes de entregar.
¡Felicidades! Ya dominas HTML a nivel profesional. El siguiente paso es darle estilo y diseño a tus páginas en el curso de CSS.