Skip to content

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:

Landing page – Café Aroma

Paso 0: planear antes de escribir ​

Antes de abrir el editor, un profesional define con el cliente:

  1. Objetivo de la página: que la gente reserve mesa o visite el local.
  2. Llamada a la acción principal (CTA): "Reserva tu mesa".
  3. Secciones: hero, menú, nosotros, opiniones, contacto/reserva.
  4. 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)
└── footer

Estructura 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.jpg

Sobre 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).

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> con aria-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>&copy; 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: sticky mantiene el encabezado visible al hacer scroll.
  • :user-invalid marca 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:

  1. Validador W3C: cero errores.
  2. Teclado: Tab muestra el skip link, recorre el menú, la CTA y todo el formulario con foco visible.
  3. Lighthouse en modo móvil: 90+ en Rendimiento y 95+ en Accesibilidad, Buenas prácticas y SEO.
  4. 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".
  5. Compartir: pega la URL en WhatsApp y revisa la vista previa.

Error común

Olvidar actualizar el año del &copy;, 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 ​

  1. Agrega una sección "Preguntas frecuentes" con tres <details> exclusivos entre "Opiniones" y "Contacto", y añádela al menú.
  2. Agrega un mapa responsivo de Google Maps (o OpenStreetMap) en la sección de contacto, con title y loading="lazy".
  3. Agrega un botón flotante de WhatsApp accesible en la esquina inferior derecha.
  4. 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, inputmode y 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.

Academia Web — aprende, copia y construye.