HTML semántico: la estructura profesional
El HTML semántico usa etiquetas que describen el papel de cada zona
de la página en lugar de cajas genéricas <div>. Un <header>
«dice» encabezado; un <div class="cabecera"> solo dice «caja cualquiera».
¿Por qué importa tanto?
- SEO: Google prioriza páginas cuya estructura entiende sin esfuerzo.
- Accesibilidad: los lectores de pantalla permiten saltar directamente al
<main>o a la<nav>. - Mantenimiento: cualquier desarrollador entiende tu código a primera vista.
El mapa de una página moderna
<body>
├── <header> ← logo, título del sitio, navegación principal
│ └── <nav> ← menús de enlaces
├── <main> ← SOLO uno por página: el contenido único
│ ├── <article> ← pieza independiente (post, noticia)
│ ├── <section> ← agrupación temática (con su h2…)
│ └── <aside> ← contenido relacionado lateral
└── <footer> ← pie del sitio: legal, contacto, redes
| Etiqueta | Úsala para… | Clave |
|---|---|---|
<header> | Cabecera del sitio o de un article/section | Puede repetirse |
<nav> | Grupos de navegación importantes | No para listas de links cualesquiera |
<main> | El contenido exclusivo de esta página | Solo UNO visible por página |
<article> | Contenido independiente: post, noticia, comentario | Tiene sentido fuera de contexto |
<section> | Capítulos o temas dentro de la página | Casi siempre lleva su heading |
<aside> | Relacionado pero secundario: widgets, ads | No es contenido principal |
<footer> | Pie del sitio o de una sección | Puede repetirse |
<time> | Fechas y horas legibles por máquinas | Usa atributo datetime |
<address> | Datos de contacto del autor | Dentro de footer suele vivir |
Ejemplo completo: entrada de blog
<body>
<header>
<p class="logo">Técnico Web</p>
<nav>
<ul>
<li><a href="/">Inicio</a></li>
<li><a href="/cursos">Cursos</a></li>
</ul>
</nav>
</header>
<main>
<article>
<header>
<h1>Aprende HTML en 2026</h1>
<p>Publicado el
<time datetime="2026-08-22">22 de agosto de 2026</time></p>
</header>
<section>
<h2>Por dónde empezar</h2>
<p>Empieza por la estructura básica…</p>
</section>
<footer>
<address>Escrito por <a href="mailto:ana@ejemplo.com">Ana García</a></address>
</footer>
</article>
</main>
<footer>
<p><small>© 2026 Técnico Web</small></p>
</footer>
</body>
article.section (con heading).div, como último recurso.Tablas de datos completas
Las tablas son para datos tabulares: notas, precios, horarios. Nunca para maquetar la página — para eso ya tienes CSS.
Anatomía completa
<table>
<caption>Notas finales del curso de HTML</caption>
<thead>
<tr>
<th scope="col">Alumno</th>
<th scope="col">Bloque 1</th>
<th scope="col">Bloque 2</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Ana</th>
<td>95</td>
<td>88</td>
</tr>
<tr>
<th scope="row">Luis</th>
<td>82</td>
<td>90</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">Media de la clase</td>
<td>89,7</td>
</tr>
</tfoot>
</table>
<caption>: título descriptivo de la tabla (accesibilidad). Va justo tras<table>.<thead>/<tbody>/<tfoot>: separan cabecera, cuerpo y resúmenes; también permiten fijar cabeceras al hacer scroll con CSS.<th scope="col|row">: marca celdas de encabezado y dice si encabezan columna o fila.colspanfusiona hacia los lados;rowspanhacia abajo. El valor indica cuántas celdas ocupa.
Fusiones: colspan y rowspan en acción
| Lunes | Martes | Miércoles | Jueves | Viernes |
| MATE | LENG | FÍS | QUIM | INGL |
| R E C R E O (colspan="5") |
| HIST | ED.FÍ | ART | MÚS | TUTO |
<tr>
<td colspan="5" class="recreo">Recreo</td>
</tr>
Formularios: recogiendo datos del usuario
Los formularios son el puente entre el usuario y tu aplicación. Cada control necesita
tres piezas: etiqueta (<label>), control
(<input>, <select>…) y el atributo name,
que es la clave con la que el dato viaja al servidor.
<form action="/registro" method="post">
<label for="email">Correo:</label>
<input type="email"
id="email" name="email"
required>
<button type="submit">Enviar</button>
</form>
GET → datos en la URL
(búsquedas, filtros)
POST → datos ocultos en el cuerpo
(contraseñas, registros)
Regla: si modifica algo → POST
La galería de input types
| Type | Para… | Extras típicos |
|---|---|---|
text | Texto libre corto | maxlength, placeholder |
email | Correos (valida formato) | multiple para varios |
password | Contraseñas (oculta caracteres) | nunca enviar por GET |
number | Números | min, max, step |
tel / url | Teléfonos / URLs | teclado adaptado en móvil |
date | Fechas con selector | min, max |
checkbox | Múltiple elección / términos | cada opción su name |
radio | Opción única dentro de grupo | mismo name = mismo grupo |
file | Subir archivos | accept=".pdf,.jpg" |
range / color | Deslizadores / selector color | valor inicial con value |
Selects, textareas y datalist
<!-- lista desplegable -->
<label for="pais">País:</label>
<select id="pais" name="pais" required>
<option value="">— Elige un país —</option>
<optgroup label="Europa">
<option value="es" selected>España</option>
<option value="pt">Portugal</option>
</optgroup>
<optgroup label="América">
<option value="mx">México</option>
</optgroup>
</select>
<!-- texto largo -->
<textarea id="msg" name="mensaje"
rows="5" placeholder="Cuéntanos…"></textarea>
<!-- autocompletado con sugerencias -->
<input list="lenguajes" name="favorito">
<datalist id="lenguajes">
<option value="HTML">
<option value="CSS">
<option value="JavaScript">
</datalist>
Validación nativa: el navegador trabaja gratis
required — campo obligatoriominlength / maxlength — longitud del textomin / max / step — rangos numéricos y fechaspattern="[0-9]{5}" + title — formatos exactosplaceholder no sustituye al label: desaparece al escribirautocomplete="email" — ayuda a rellenar rápidoSi algo falla, el navegador muestra el mensaje, enfoca el campo y no envía el formulario. Sin una sola línea de JavaScript.
disabled desactiva el campo y sus datos no se envían;
readonly solo bloquea la edición pero el valor sí viaja. Agrupa opciones relacionadas
con <fieldset> + <legend>: mejora la comprensión y la accesibilidad.Audio, vídeo e iframes
Audio con formatos alternativos
<figure>
<audio controls preload="metadata">
<source src="audio/ep1.mp3" type="audio/mpeg">
<source src="audio/ep1.ogg" type="audio/ogg">
Tu navegador no soporta audio nativo.
</audio>
<figcaption>Episodio 1 — Primeros pasos</figcaption>
</figure>
Vídeo con subtítulos accesibles
<video controls
poster="img/portada.jpg"
width="720" height="405"
preload="metadata">
<source src="video/demo.mp4" type="video/mp4">
<source src="video/demo.webm" type="video/webm">
<track src="video/subs-es.vtt" kind="subtitles"
srclang="es" label="Español" default>
<track src="video/subs-en.vtt" kind="subtitles"
srclang="en" label="English">
<p>Descarga la
<a href="video/transcripcion.txt" download>transcripción</a>.
</p>
</video>
controlsmuestra los botones nativos; sin él, el vídeo es invisible e inmanejable.poster: imagen de portada antes de dar a play.preload="metadata"carga solo lo mínimo: la página vuela.- Varios
<source>= el navegador elige el primer formato que entienda (mp4 + webm cubre todo). <track kind="captions">añade subtítulos: obligatorio para accesibilidad seria.
autoplay con sonido: los navegadores lo bloquean, los usuarios lo odian.
Si lo usas, debe ser siempre autoplay muted loop (fondos animados).Iframes: incrustando otros sitios
<!-- vídeo de YouTube -->
<iframe
src="https://www.youtube.com/embed/ID_DEL_VIDEO"
title="Vídeo tutorial de HTML"
width="720" height="405"
loading="lazy"
allowfullscreen></iframe>
<!-- mapa de Google Maps (comparte → insertar) -->
<iframe
src="https://www.google.com/maps/embed?pb=..."
title="Mapa de nuestra academia en Madrid"
width="720" height="360"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
allowfullscreen></iframe>
titleen cada iframe es imprescindible: los lectores de pantalla anuncian qué contiene.loading="lazy"evita cargar YouTube/Maps hasta llegar a ellos: ahorra muchísimo.- Los iframes cargan código ajeno: usa servicios de confianza y considera
sandboxpara contenido no confiable.
✍️ Los 12 ejercicios del bloque
Del 1 al 11 consolidas lecciones; el 12 es el reto del bloque: el formulario de registro completo que valida la unidad.
1 Esqueleto de portada de noticias
Crea la estructura semántica (solo etiquetas, contenido breve) de una portada de periódico: cabecera con menú, contenido principal con dos noticias independientes, columna lateral de «lo más leído» y pie legal.
Ver solución
<header>
<h1>El Diario Web</h1>
<nav><ul>
<li><a href="#">Portada</a></li>
<li><a href="#">Deportes</a></li>
</ul></nav>
</header>
<main>
<article><h2>Noticia 1</h2><p>…</p></article>
<article><h2>Noticia 2</h2><p>…</p></article>
<aside><h2>Lo más leído</h2><ul>
<li><a href="#">Artículo popular</a></li>
</ul></aside>
</main>
<footer><small>© 2026 El Diario Web</small></footer>
2 Rescatar una sopa de divs
Este código funciona pero es ilegible. Reescríbelo con etiquetas semánticas equivalentes:
<div class="top">Mi Tienda</div>
<div class="menu"><a href="#">Inicio</a></div>
<div class="contenido">
<div class="post">Ofertas de agosto</div>
<div class="lateral">Envíos gratis hoy</div>
</div>
<div class="pie">Contacto: info@tienda.es</div>
Ver solución
<header>Mi Tienda</header>
<nav><a href="#">Inicio</a></nav>
<main>
<article>Ofertas de agosto</article>
<aside>Envíos gratis hoy</aside>
</main>
<footer>Contacto: <address>info@tienda.es</address></footer>
3 Noticia de nivel periodístico
Escribe un <article> de noticia con: cabecera interna (titular h2 + fecha
legible con <time datetime="2026-08-20T09:30">), dos párrafos y pie interno
con <address> del autor.
Ver solución
<article>
<header>
<h2>La ciudad estrena carril bici inteligente</h2>
<p>
<time datetime="2026-08-20T09:30">
20 de agosto de 2026, 09:30
</time>
</p>
</header>
<p>El nuevo carril conecta once barrios…</p>
<p>Los vecinos celebran la medida…</p>
<footer>
<address>
Por <a href="mailto:marta@diario.es">Marta Ruiz</a>
</address>
</footer>
</article>
4 Horario semanal perfecto
Tabla de un horario de clase (L–V × mañana/tarde) con caption,
thead con scope="col", tbody con horas en
th scope="row", una fila «Recreo» fusionada con colspan y
tfoot con total de horas.
Ver solución
<table>
<caption>Horario semanal — Grupo A</caption>
<thead><tr>
<th scope="col">Tramo</th>
<th scope="col">Lun</th>
<th scope="col">Mar</th>
<th scope="col">Mié</th>
<th scope="col">Jue</th>
<th scope="col">Vie</th>
</tr></thead>
<tbody>
<tr>
<th scope="row">09–11</th>
<td>MATE</td><td>LENG</td><td>FÍS</td><td>QUIM</td><td>INGL</td>
</tr>
<tr><td colspan="6">Recreo</td></tr>
<tr>
<th scope="row">12–14</th>
<td>HIST</td><td>ED.FÍ</td><td>ART</td><td>MÚS</td><td>TUTO</td>
</tr>
</tbody>
<tfoot><tr>
<td colspan="6">Total: 10 horas semanales</td>
</tr></tfoot>
</table>
5 Facturación con rowspan
Tabla de ventas donde la categoría «Hardware» abarca dos filas (rowspan="2")
y el pie suma totales por trimestre.
Ver solución
<table>
<caption>Ventas Q3 2026 (miles €)</caption>
<thead><tr>
<th scope="col">Categoría</th>
<th scope="col">Producto</th>
<th scope="col">Julio</th>
<th scope="col">Agosto</th>
</tr></thead>
<tbody>
<tr>
<th rowspan="2" scope="row">Hardware</th>
<td>Teclados</td><td>12</td><td>15</td>
</tr>
<tr><td>Ratones</td><td>9</td><td>11</td></tr>
</tbody>
<tfoot><tr>
<td colspan="2">Total</td>
<td>21</td><td>26</td>
</tr></tfoot>
</table>
6 Contacto accesible
Formulario de contacto dentro de un <section id="contacto"> con título,
fieldset «Mensaje», nombre, email, asunto select y textarea, todo con labels correctos.
Ver solución
<section id="contacto">
<h2>Contacta con nosotros</h2>
<form action="/contacto" method="post">
<fieldset>
<legend>Mensaje</legend>
<label for="cNombre">Nombre:</label>
<input type="text" id="cNombre" name="nombre" required>
<label for="cEmail">Email:</label>
<input type="email" id="cEmail" name="email" required>
<label for="cAsunto">Asunto:</label>
<select id="cAsunto" name="asunto">
<option value="consulta">Consulta general</option>
<option value="soporte">Soporte técnico</option>
</select>
<label for="cMsg">Mensaje:</label>
<textarea id="cMsg" name="mensaje" rows="6" required></textarea>
<button type="submit">Enviar</button>
</fieldset>
</form>
</section>
7 Encuesta de gustos tech
Una encuesta con: radios (¿frontend o backend?, un solo grupo), checkboxes (lenguajes que
conoces, selección múltiple), select de país con optgroup por continente y un
input con datalist de sugerencias.
Ver solución
<fieldset>
<legend>¿Qué te llama más?</legend>
<input type="radio" id="fe" name="rama" value="front" checked>
<label for="fe">Frontend</label>
<input type="radio" id="be" name="rama" value="back">
<label for="be">Backend</label>
</fieldset>
<fieldset>
<legend>Lenguajes que conoces</legend>
<input type="checkbox" id="html" name="langs" value="html">
<label for="html">HTML</label>
<input type="checkbox" id="css" name="langs" value="css">
<label for="css">CSS</label>
</fieldset>
<label for="paisE">País:</label>
<select id="paisE" name="pais">
<optgroup label="Europa">
<option value="es">España</option>
</optgroup>
<optgroup label="América">
<option value="mx">México</option>
</optgroup>
</select>
<label for="nextLang">Siguiente lenguaje:</label>
<input list="sugerencias" id="nextLang" name="siguiente">
<datalist id="sugerencias">
<option value="JavaScript">
<option value="PHP">
<option value="Python">
</datalist>
8 Validación nativa exprés
Un mini-registro donde: el email use type=email required, la contraseña
minlength="8", el CP pattern="[0-9]{5}" con title
explicativo, la edad type=number min="13" max="120" y la web personal
type=url opcional.
Ver solución
<form action="/registro-rapido" method="post">
<label for="rEmail">Email:</label>
<input type="email" id="rEmail" name="email"
autocomplete="email" required>
<label for="rPass">Contraseña:</label>
<input type="password" id="rPass" name="pass"
minlength="8" required>
<label for="rCP">Código postal:</label>
<input type="text" id="rCP" name="cp"
pattern="[0-9]{5}"
title="Introduce exactamente 5 dígitos" required>
<label for="rEdad">Edad:</label>
<input type="number" id="rEdad" name="edad"
min="13" max="120">
<label for="rWeb">Web personal (opcional):</label>
<input type="url" id="rWeb" name="web"
placeholder="https://…">
<button type="submit">Crear cuenta</button>
</form>
9 Pódcast publicable
Publica un episodio: <figure> con <audio controls> que ofrezca
MP3 y OGG, preload="metadata" y figcaption con número y título del episodio.
Ver solución
<figure>
<audio controls preload="metadata">
<source src="media/ep07.mp3" type="audio/mpeg">
<source src="media/ep07.ogg" type="audio/ogg">
Tu navegador no reproduce audio nativo.
</audio>
<figcaption>
Episodio 07 — Formularios que se validan solos
</figcaption>
</figure>
10 Vídeo 100% accesible
Embebe un tutorial con: poster propio, mp4 + webm, subtítulos español (por defecto) e inglés
mediante <track>, enlace descargable a la transcripción y sin autoplay.
Ver solución
<figure>
<video controls poster="media/portada-curso.jpg"
width="720" height="405"
preload="metadata">
<source src="media/clase01.mp4" type="video/mp4">
<source src="media/clase01.webm" type="video/webm">
<track src="media/clase01-es.vtt" kind="subtitles"
srclang="es" label="Español" default>
<track src="media/clase01-en.vtt" kind="subtitles"
srclang="en" label="English">
<p>¿Sin soporte? Descarga la
<a href="media/clase01-transcripcion.txt" download>
transcripción completa</a>.</p>
</video>
<figcaption>Clase 01 — Estructura semántica</figcaption>
</figure>
11 Página «Dónde estamos»
Crea una sección con mapa de Google Maps incrustado (title descriptivo +
loading="lazy") y, debajo, un vídeo de presentación de YouTube también perezoso.
Ver solución
<section id="donde-estamos">
<h2>Dónde estamos</h2>
<iframe
src="https://www.google.com/maps/embed?pb=TU_CODIGO"
title="Mapa con la ubicación de la academia"
width="720" height="360"
style="border:0"
loading="lazy"
allowfullscreen></iframe>
<h3>Conócenos en 2 minutos</h3>
<iframe
src="https://www.youtube.com/embed/TU_ID"
title="Vídeo de presentación de la academia"
width="720" height="405"
loading="lazy"
allowfullscreen></iframe>
</section>
12 🏆 Reto del bloque: formulario de registro completo
Crea registro.html: página semántica con <main> y un formulario
method="post" dividido en tres <fieldset>:
- Datos personales: nombre (required), email (type email + required), fecha de nacimiento (type date) y edad (number, 13–120)
- Credenciales: usuario, contraseña (minlength 8, required) y repetir contraseña
- Preferencias: radios de nivel (principiante checked / intermedio / avanzado), select de país requerido con optgroup y placeholder vacío, checkbox «acepto los términos» required
Todo con labels correctos, autocomplete donde toque y validación 100% nativa.
Ver solución orientativa
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Registro — Academia Online</title>
</head>
<body>
<main>
<h1>Crea tu cuenta</h1>
<form action="/registro" method="post">
<fieldset>
<legend>Datos personales</legend>
<label for="nombre">Nombre:</label>
<input type="text" id="nombre" name="nombre" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email"
autocomplete="email" required>
<label for="nacimiento">Fecha de nacimiento:</label>
<input type="date" id="nacimiento" name="nacimiento"
min="1930-01-01">
<label for="edad">Edad:</label>
<input type="number" id="edad" name="edad"
min="13" max="120">
</fieldset>
<fieldset>
<legend>Credenciales</legend>
<label for="usuario">Usuario:</label>
<input type="text" id="usuario" name="usuario"
autocomplete="username" required>
<label for="pass">Contraseña:</label>
<input type="password" id="pass" name="pass"
minlength="8" autocomplete="new-password" required>
<label for="pass2">Repite la contraseña:</label>
<input type="password" id="pass2" name="pass2"
minlength="8" required>
</fieldset>
<fieldset>
<legend>Preferencias</legend>
<input type="radio" id="n1" name="nivel" value="inicial" checked>
<label for="n1">Empiezo desde cero</label>
<input type="radio" id="n2" name="nivel" value="medio">
<label for="n2">Nivel intermedio</label>
<input type="radio" id="n3" name="nivel" value="pro">
<label for="n3">Soy desarrollador/a</label>
<label for="pais">País:</label>
<select id="pais" name="pais" required>
<option value="">— Elige tu país —</option>
<optgroup label="Europa">
<option value="es">España</option>
<option value="pt">Portugal</option>
</optgroup>
<optgroup label="América">
<option value="mx">México</option>
<option value="ar">Argentina</option>
</optgroup>
</select>
<input type="checkbox" id="terminos" name="terminos" required>
<label for="terminos">Acepto los términos y condiciones</label>
</fieldset>
<button type="submit">Registrarme</button>
</form>
</main>
</body>
</html>
📝 Examen del Bloque 2
Como siempre: test teórico autoevaluable (mínimo 70 %) + reto práctico con checklist de autocorrección.
Parte B · Reto práctico final
Construye el ejercicio 12 («registro.html») sin mirar la solución y valida tú mismo/a. Mínimo para aprobar: 7 de 8 casillas.
Cumplidos: 0 / 8