Mapas de imagen, iframes y divisores de contenido

Por: Artiko
htmlcssjavascriptdomfrontendiframeimage-mapsemanticaaccesibilidadsandbox

Mapas de imagen, iframes y divisores de contenido

En el capitulo 6 desarmamos el enlace: vimos que una URL es una direccion con partes bien definidas, que las rutas relativas se resuelven contra el documento actual, y que target decide en que contexto se abre el destino. Todo eso tenia un punto en comun: el area que el usuario podia pulsar era el texto o la imagen completa envuelta por un <a>.

Este capitulo rompe ese supuesto en tres direcciones distintas.

Primero, vamos a partir una sola imagen en varias zonas pulsables independientes usando <map> y <area>: un mapa de imagen. Segundo, vamos a incrustar una pagina completa dentro de otra con <iframe>, lo que nos obliga a hablar de contextos de navegacion anidados y, sobre todo, de seguridad. Y tercero, vamos a ordenar el documento con los contenedores que le dan forma: <hr>, <div>, <span>, <header>, <nav>, <main>, <section>, <article>, <aside> y <footer>.

Los tres temas parecen inconexos, pero comparten una idea: el navegador no solo dibuja pixeles, tambien construye un mapa de regiones interactivas y una estructura de significado. Los mapas de imagen definen regiones dentro de una imagen; los iframes definen regiones que son documentos completos; los divisores definen regiones de significado dentro del propio documento.

Que vas a poder hacer al terminar

  • Convertir una ilustracion, un plano o un diagrama en un panel de enlaces sin recortar la imagen en pedazos.
  • Calcular e interpretar las coordenadas de un <area> para las cuatro formas disponibles.
  • Reconocer las limitaciones de los mapas de imagen y aplicar la alternativa moderna basada en CSS o SVG.
  • Incrustar un video, un mapa o un documento externo con <iframe> y entender que se ejecuta en un contexto separado.
  • Restringir lo que un contenido incrustado puede hacer usando sandbox, allow y referrerpolicy.
  • Impedir que tu propio sitio sea incrustado por terceros.
  • Elegir el contenedor correcto entre div, section, article y compania, sin caer en la sopa de div.

Parte 1: mapas de imagen

El problema que resuelven

Imagina una imagen de un tablero de juego con cuatro personajes. Quieres que al pulsar cada personaje el usuario vaya a una pagina distinta. Con lo que sabemos hasta ahora hay dos caminos malos:

  1. Recortar la imagen en cuatro archivos y envolver cada uno en un <a>. Funciona solo si las zonas son rectangulos alineados en una grilla, y multiplica las peticiones al servidor.
  2. Poner una sola imagen dentro de un <a>. Entonces toda la imagen apunta a un unico destino.

Un mapa de imagen (image map) resuelve el caso real: una sola imagen, varias zonas pulsables, cada una con su propia forma y su propio destino.

Anatomia de un mapa de imagen

Un mapa de imagen siempre son tres piezas trabajando juntas:

PiezaEtiquetaRol
La imagen<img usemap="#nombre">Muestra los pixeles y declara que usa un mapa
El mapa<map name="nombre">Contenedor con nombre que agrupa las zonas
Las zonas<area>Cada region pulsable, con forma, coordenadas y destino

El vinculo entre la imagen y el mapa es puramente por nombre: el atributo usemap de la imagen contiene una almohadilla seguida del valor exacto del atributo name del <map>. Es el mismo mecanismo de fragmento que vimos en el capitulo anterior con los anclajes internos, pero aqui no navega a ninguna parte: solo identifica el mapa.

flowchart TD
    A["Usuario hace clic en (x, y)<br/>sobre la imagen"] --> B{"La img tiene<br/>usemap?"}
    B -- No --> C["Comportamiento normal:<br/>si esta dentro de un a, navega;<br/>si no, no pasa nada"]
    B -- "Si: usemap='#mariokart'" --> D["Busca en el documento<br/>map name='mariokart'"]
    D --> E{"Existe ese map?"}
    E -- No --> C
    E -- Si --> F["Recorre los area<br/>en orden de aparicion"]
    F --> G{"El punto (x, y) cae<br/>dentro de esta area?"}
    G -- No --> H{"Quedan mas area?"}
    H -- Si --> F
    H -- No --> I["Ninguna zona coincide:<br/>clic ignorado"]
    G -- Si --> J{"El area tiene href?"}
    J -- Si --> K["Navega al href<br/>respetando target"]
    J -- No --> L["Zona muerta:<br/>bloquea las de abajo<br/>y no navega"]

Lo importante del diagrama: el navegador recorre las areas en el orden en que estan escritas y se queda con la primera que contenga el punto. El orden importa, y mucho.

El sistema de coordenadas

Las coordenadas de un <area> se expresan en pixeles y se miden desde la esquina superior izquierda de la imagen. El eje X crece hacia la derecha y el eje Y crece hacia abajo. Esto ultimo suele confundir a quien viene de matematicas: en la pantalla el eje Y esta invertido respecto al plano cartesiano habitual.

graph LR
    subgraph IMG["Imagen de 800 x 600 px"]
        direction TB
        O["(0,0) esquina superior izquierda"]
        X["(800,0) esquina superior derecha"]
        Y["(0,600) esquina inferior izquierda"]
        Z["(800,600) esquina inferior derecha"]
        O -->|"X crece -->"| X
        O -->|"Y crece hacia abajo"| Y
        X --> Z
        Y --> Z
    end

Para obtener coordenadas reales tienes tres opciones practicas:

  1. Un editor de imagenes como GIMP, Krita o Photoshop: casi todos muestran la posicion del cursor en pixeles en una barra de estado.
  2. Las herramientas de desarrollo del navegador: abre la imagen sola en una pestana y usa la extension de regla, o mide con el inspector.
  3. JavaScript, que es lo mas comodo cuando estas afinando. Este fragmento imprime en consola la coordenada exacta de cada clic sobre la imagen:
<h1>Haz clic sobre la imagen para leer sus coordenadas</h1>
<p id="salida">Todavia no has hecho clic.</p>
<img id="lienzo" src="tablero.png" alt="Tablero del juego">

<script>
  const imagen = document.getElementById('lienzo');
  const salida = document.getElementById('salida');

  imagen.addEventListener('click', function (evento) {
    // Rectangulo que ocupa la imagen en la pantalla.
    const caja = imagen.getBoundingClientRect();

    // Coordenada del clic relativa a la esquina superior izquierda.
    const xVisible = evento.clientX - caja.left;
    const yVisible = evento.clientY - caja.top;

    // Conversion a pixeles reales del archivo, por si la imagen
    // se esta mostrando escalada.
    const escalaX = imagen.naturalWidth / caja.width;
    const escalaY = imagen.naturalHeight / caja.height;

    const xReal = Math.round(xVisible * escalaX);
    const yReal = Math.round(yVisible * escalaY);

    salida.textContent =
      'Visible: ' + Math.round(xVisible) + ',' + Math.round(yVisible) +
      '  |  Real en el archivo: ' + xReal + ',' + yReal;
    console.log(xReal + ',' + yReal);
  });
</script>

Fijate en la distincion entre naturalWidth (el ancho real del archivo) y getBoundingClientRect().width (el ancho con el que se esta pintando). Volveremos a ella cuando hablemos de las limitaciones.

Las cuatro formas de <area>

El atributo shape acepta cuatro valores, y cada uno interpreta coords de forma distinta.

shapeNumero de valores en coordsSignificado de los valoresUso tipico
rect4x1,y1,x2,y2: esquina superior izquierda y esquina inferior derechaBotones, cuadros, celdas
circle3x,y,r: centro y radioCaras, iconos redondos, planetas
poly6 o mas (pares)x1,y1,x2,y2,x3,y3,...: vertices en ordenPaises, piezas irregulares, siluetas
defaultningunoToda el area de la imagen no cubierta por las anterioresZona de respaldo

Un detalle que se olvida siempre: poly necesita al menos tres pares de coordenadas, o sea seis numeros, porque con menos no hay poligono. El navegador cierra el poligono automaticamente uniendo el ultimo vertice con el primero: no repitas el punto inicial al final.

Veamos los cuatro casos en un ejemplo completo. Este documento funciona tal cual si colocas junto a el una imagen llamada mapa-mundo.png:

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Las cuatro formas de area</title>
  </head>
  <body>
    <h1>Panel de tecnologias</h1>

    <map name="panel-tecnologias">
      <area
        shape="rect"
        coords="93,178,385,457"
        href="https://developer.mozilla.org/es/docs/Web/JavaScript"
        target="_blank"
        rel="noopener"
        alt="Documentacion de JavaScript">

      <area
        shape="circle"
        coords="199,611,75"
        href="https://developer.mozilla.org/es/docs/Web/CSS"
        target="_blank"
        rel="noopener"
        alt="Documentacion de CSS">

      <area
        shape="poly"
        coords="520,140,700,220,660,420,470,380"
        href="https://developer.mozilla.org/es/docs/Web/HTML"
        target="_blank"
        rel="noopener"
        alt="Documentacion de HTML">

      <area
        shape="default"
        href="/tecnologias/superclubnet/00-indice/"
        alt="Volver al indice del curso">
    </map>

    <img
      src="mapa-mundo.png"
      usemap="#panel-tecnologias"
      width="800"
      height="700"
      alt="Panel con tres zonas: JavaScript, CSS y HTML">
  </body>
</html>

Tres observaciones sobre este codigo:

  • El <area> con shape="default" va al final. Como el navegador se queda con la primera coincidencia y default cubre toda la imagen, ponerlo arriba haria que ninguna otra zona funcionara jamas. Es el error clasico.
  • Cada <area> con href lleva su propio alt. No es opcional: es el texto que lee un lector de pantalla y el equivalente del texto de un enlace normal. Un <area> con href y sin alt es HTML invalido.
  • El rel="noopener" acompana a target="_blank" por la misma razon que vimos en el capitulo anterior: evita que la pagina destino pueda manipular la ventana de origen a traves de window.opener. Los navegadores modernos ya lo aplican por defecto, pero declararlo es gratis y explicito.

Zonas muertas: cuando no quieres un enlace

A veces necesitas que una region no haga nada, pero que tampoco caiga en la zona default. Historicamente esto se hacia con un atributo nohref, que aparece en material antiguo. Ese atributo fue eliminado del estandar: hoy la forma correcta es simplemente omitir href.

<map name="tablero">
  <!-- El centro del tablero no lleva a ninguna parte,
       pero bloquea que el clic caiga en la zona default. -->
  <area shape="circle" coords="400,350,60" alt="">

  <area shape="rect" coords="0,0,200,200" href="/esquina-a/" alt="Esquina A">

  <area shape="default" href="/ayuda/" alt="Ayuda general del tablero">
</map>

Un <area> sin href no es un enlace, no recibe foco de teclado y su alt debe quedar vacio. Sirve exclusivamente como escudo: intercepta el clic para que no lo tome una zona posterior.

Atributos completos de <area>

Como <area> es, conceptualmente, un <a> con forma geometrica, comparte casi todos sus atributos.

AtributoPara que sirve
shapeForma de la zona: rect, circle, poly o default
coordsCoordenadas en pixeles, separadas por comas
hrefDestino del enlace. Si falta, la zona no es un enlace
altTexto alternativo. Obligatorio cuando hay href
targetContexto de navegacion: _self, _blank, _parent, _top o el name de un iframe
relRelacion con el destino: noopener, noreferrer, nofollow, etc.
downloadDescarga el recurso en vez de navegar hacia el
pingLista de URL a las que notificar el clic
referrerpolicyCuanta informacion de origen se envia al destino

Es exactamente el vocabulario del capitulo 6 aplicado a una geometria. Si entendiste <a>, entendiste <area>.

La gran limitacion: los mapas no escalan

Las coordenadas de un <area> son numeros fijos en pixeles. Si muestras la imagen a su tamano original todo cuadra. Pero en cuanto haces esto:

img {
  max-width: 100%;
  height: auto;
}

la imagen se encoge en pantallas angostas y, en la practica, los navegadores no reescalan las coordenadas del mapa. El resultado es que las zonas pulsables quedan desplazadas respecto a lo que el usuario ve. En un movil, pulsar el personaje de la izquierda puede activar el enlace de otro.

Esta es la razon por la que los mapas de imagen clasicos se usan poco en sitios modernos. Existen tres salidas.

Salida 1: fijar el tamano de la imagen. Sirve solo si controlas el contexto (una pantalla de kiosco, un panel interno).

Salida 2: recalcular las coordenadas con JavaScript. Guardas las coordenadas originales y las multiplicas por el factor de escala cada vez que cambia el tamano:

<map name="zonas">
  <area shape="rect" coords="93,178,385,457" href="/js/" alt="JavaScript">
  <area shape="circle" coords="199,611,75" href="/css/" alt="CSS">
</map>
<img id="tablero" src="tablero.png" usemap="#zonas"
     style="max-width:100%;height:auto;display:block"
     alt="Tablero de tecnologias">

<script>
  const imagen = document.getElementById('tablero');
  const areas = document.querySelectorAll('map[name="zonas"] area');

  // Guardamos las coordenadas originales una sola vez.
  const originales = new Map();
  areas.forEach(function (area) {
    originales.set(area, area.coords.split(',').map(Number));
  });

  function reescalar() {
    if (!imagen.naturalWidth) return;
    const factor = imagen.clientWidth / imagen.naturalWidth;

    areas.forEach(function (area) {
      const base = originales.get(area);
      const nuevas = base.map(function (valor) {
        return Math.round(valor * factor);
      });
      area.coords = nuevas.join(',');
    });
  }

  // Recalculamos al cargar la imagen y cada vez que cambie de tamano.
  imagen.addEventListener('load', reescalar);
  if (imagen.complete) reescalar();
  new ResizeObserver(reescalar).observe(imagen);
</script>

ResizeObserver es una API real del navegador que avisa cuando un elemento cambia de tamano, sea por el viewport, por un contenedor flexible o por lo que sea. Es mas fiable que escuchar el evento resize de la ventana.

Un matiz importante: este reescalado funciona bien para rect y poly, y para circle solo si el factor horizontal y el vertical son iguales, que es lo normal cuando usas height: auto. Si deformas la imagen con proporciones distintas, un circulo deberia convertirse en elipse y <area> no sabe hacer eso.

Salida 3: no usar mapa de imagen. Es la opcion mas robusta. Colocas la imagen como fondo o como <img> dentro de un contenedor con position: relative y superpones enlaces normales posicionados en porcentajes. Al ser porcentajes, escalan solos:

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Zonas pulsables sin map</title>
    <style>
      .escenario {
        position: relative;
        max-width: 800px;
        margin: 0 auto;
      }

      .escenario img {
        display: block;
        width: 100%;
        height: auto;
      }

      .zona {
        position: absolute;
        display: block;
        border: 2px solid transparent;
        border-radius: 4px;
      }

      .zona:hover,
      .zona:focus-visible {
        border-color: #ff4081;
        background: rgba(255, 64, 129, 0.15);
        outline: none;
      }

      /* Coordenadas expresadas como porcentaje del contenedor:
         escalan automaticamente con la imagen. */
      .zona-js {
        left: 11.6%;
        top: 25.4%;
        width: 36.5%;
        height: 39.9%;
      }

      .zona-css {
        left: 15.5%;
        top: 76.6%;
        width: 18.8%;
        height: 21.4%;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <div class="escenario">
      <img src="tablero.png" alt="Tablero con dos zonas interactivas">

      <a class="zona zona-js" href="/tecnologias/superclubnet/08-javascript-tipos-variables-y-strings/">
        <span class="visually-hidden">JavaScript</span>
      </a>

      <a class="zona zona-css" href="/tecnologias/superclubnet/00-indice/">
        <span class="visually-hidden">CSS</span>
      </a>
    </div>
  </body>
</html>

Para convertir un pixel en porcentaje: divide la coordenada por el ancho (o alto) real de la imagen y multiplica por cien. Con una imagen de 800 x 700, la coordenada X de 93 px equivale a 93 / 800 * 100 = 11.6%.

Esta version tiene tres ventajas sobre el mapa clasico: escala sola, permite estados visuales de hover y foco (algo que <area> no ofrece de forma comoda), y usa enlaces normales que cualquier herramienta entiende.

Si necesitas formas irregulares con esta tecnica, la herramienta correcta es un <svg> inline con elementos <path> envueltos en <a>, porque el SVG escala por definicion.

Comparacion rapida de tecnicas

TecnicaFormas irregularesEscala solaEstados hover/focoComplejidad
map + area clasicoSi (poly)NoMuy limitadosBaja
map + area + JavaScriptSiSiMuy limitadosMedia
Enlaces absolutos en porcentajesSolo rectangulos y elipsesSiCompletosBaja
SVG inline con <a> y <path>SiSiCompletosMedia

Parte 2: la etiqueta <iframe>

De los frames a los iframes

En los primeros anos de la web existian <frameset> y <frame>: se partia la ventana del navegador en varias zonas y cada una cargaba un documento distinto. El patron tipico era un menu fijo a la izquierda y el contenido a la derecha. Funcionaba, pero traia problemas serios: la URL de la barra de direcciones no reflejaba lo que se estaba viendo, no se podia guardar un marcador util, el boton de atras se comportaba de forma erratica y los buscadores indexaban fragmentos sueltos sin contexto.

Ambas etiquetas quedaron obsoletas. Lo que sobrevivio fue <iframe>, el inline frame: en vez de partir la ventana, inserta un documento completo dentro del flujo de la pagina, como si fuera una imagen mas.

Que es realmente un iframe

Un <iframe> no es un contenedor de HTML. Es una ventana del navegador en miniatura: crea un contexto de navegacion anidado con su propio documento, su propio DOM, su propio historial, sus propios estilos y su propio JavaScript.

flowchart TB
    subgraph VENTANA["Ventana del navegador"]
        subgraph PADRE["Documento padre: tusitio.cl"]
            H["head, body, DOM propio"]
            CSSP["CSS propio"]
            JSP["JavaScript propio<br/>window.top"]
            IF["elemento iframe<br/>(una caja en el layout)"]
        end
    end

    IF --> HIJO

    subgraph HIJO["Contexto anidado: youtube.com/embed/..."]
        H2["head, body, DOM independiente"]
        CSS2["CSS independiente"]
        JS2["JavaScript independiente<br/>window.parent apunta al padre"]
        HIST["Historial de navegacion propio"]
    end

    JSP -. "bloqueado por<br/>politica de mismo origen" .-x JS2
    JSP -. "solo mensajes<br/>via postMessage" .-> JS2

De ese diagrama salen las tres consecuencias practicas mas importantes:

  1. El CSS de tu pagina no entra al iframe. Si el contenido incrustado se ve feo, no puedes arreglarlo con tus hojas de estilo. Solo controlas el tamano y el borde de la caja exterior.
  2. El JavaScript esta aislado si los origenes son distintos. La politica de mismo origen impide que tu codigo lea el DOM del iframe y viceversa. La unica via de comunicacion es postMessage.
  3. Cada iframe descarga un documento completo. Con su HTML, su CSS, sus fuentes, sus imagenes y sus scripts. Cinco iframes son, en costo, cinco paginas cargando a la vez.

El ejemplo canonico: incrustar un video

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Video incrustado</title>
  </head>
  <body>
    <h1>Repaso del capitulo</h1>

    <iframe
      width="560"
      height="315"
      src="https://www.youtube.com/embed/LeYLP5uerGQ"
      title="Video explicativo del capitulo 7"
      loading="lazy"
      referrerpolicy="strict-origin-when-cross-origin"
      allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
      allowfullscreen>
    </iframe>
  </body>
</html>

Repasemos atributo por atributo, porque cada uno hace algo concreto:

AtributoQue hace
srcURL del documento a cargar. Puede ser absoluta o relativa, con las reglas del capitulo 6
srcdocHTML en linea que se carga en lugar de src. Tiene prioridad sobre src
titleDescripcion del contenido incrustado. Es lo que anuncia un lector de pantalla al llegar al marco
width / heightTamano en pixeles CSS de la caja
nameNombre del contexto, para poder apuntarle con target desde un enlace
loadingeager (por defecto) o lazy, que retrasa la carga hasta que el marco se acerca a la pantalla
allowPolitica de permisos: que capacidades del navegador puede usar el contenido
allowfullscreenPermite que el contenido pida pantalla completa
referrerpolicyCuanta informacion sobre tu URL se envia al servidor del iframe
sandboxRestringe severamente lo que el contenido puede hacer

Notaras que el ejemplo no lleva frameborder="0". Ese atributo esta obsoleto: el borde se quita con CSS, que es donde corresponde.

iframe {
  border: 0;
}

El atributo title no es decorativo. Sin el, un usuario navegando con lector de pantalla escucha “marco” y nada mas. Con el, escucha de que se trata el marco antes de decidir si entra.

Iframes que mantienen su proporcion

Un iframe con width y height fijos se desborda en pantallas angostas. La solucion moderna es una linea de CSS:

.video {
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
}

.video iframe {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  border: 0;
  border-radius: 8px;
}
<div class="video">
  <iframe
    src="https://www.youtube.com/embed/LeYLP5uerGQ"
    title="Video del capitulo"
    loading="lazy"
    allowfullscreen>
  </iframe>
</div>

La propiedad aspect-ratio reserva el alto proporcional al ancho, asi que el marco mantiene su forma 16:9 en cualquier pantalla. Antes de que existiera habia que usar un truco con padding-bottom: 56.25%, que todavia veras en codigo antiguo.

srcdoc: contenido incrustado sin archivo externo

Si quieres una vista previa aislada sin cargar nada de la red, srcdoc recibe HTML directamente:

<iframe
  title="Vista previa del componente"
  width="400"
  height="200"
  srcdoc="
    <!DOCTYPE html>
    <html lang='es'>
      <head><meta charset='utf-8'></head>
      <body style='font-family: sans-serif; background: #111; color: #eee;'>
        <p>Este documento vive dentro del atributo srcdoc.</p>
      </body>
    </html>
  ">
</iframe>

Fijate en un detalle mecanico: el valor de srcdoc va entre comillas dobles, asi que dentro del HTML incrustado usamos comillas simples. Si necesitas comillas dobles dentro, hay que escaparlas como &quot;.

srcdoc es util para documentacion de componentes, para sandboxes de ejemplos de codigo, y para aislar contenido que no controlas.

Iframes y target: la conexion con el capitulo anterior

En el capitulo 6 vimos los valores de target y mencionamos _parent y _top sin poder explicarlos del todo, porque no habiamos visto los iframes. Ahora si.

stateDiagram-v2
    [*] --> Top: Documento raiz (la pestana)
    Top --> Padre: contiene
    Padre --> Hijo: contiene un iframe

    note right of Top
        target="_top"
        Reemplaza toda la pestana.
        Rompe cualquier anidamiento.
    end note

    note right of Padre
        target="_parent"
        Reemplaza el documento que
        contiene a este iframe.
    end note

    note right of Hijo
        target="_self" (por defecto)
        Navega dentro del mismo iframe.

        target="_blank"
        Abre una pestana nueva.

        target="nombreDelIframe"
        Navega dentro de ESE iframe.
    end note

El ultimo caso es el mas util y el menos conocido: un enlace puede cargar su destino dentro de un iframe concreto, si el iframe tiene name y el enlace apunta a ese nombre. Es, en esencia, el viejo patron de menu con frames, pero hecho correctamente:

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Visor con menu</title>
    <style>
      body { font-family: system-ui, sans-serif; margin: 0; }
      .disposicion { display: flex; min-height: 100vh; }
      nav { width: 200px; padding: 1rem; background: #1b1b1f; }
      nav a { display: block; color: #9ecbff; padding: 0.5rem 0; }
      iframe { flex: 1; border: 0; }
    </style>
  </head>
  <body>
    <div class="disposicion">
      <nav aria-label="Secciones de la documentacion">
        <a href="/tecnologias/superclubnet/00-indice/" target="visor">Indice</a>
        <a href="/tecnologias/superclubnet/06-enlaces-uri-url-y-rutas/" target="visor">Capitulo 6</a>
        <a href="/tecnologias/superclubnet/08-javascript-tipos-variables-y-strings/" target="visor">Capitulo 8</a>
      </nav>

      <iframe
        name="visor"
        title="Visor de documentacion"
        src="/tecnologias/superclubnet/00-indice/">
      </iframe>
    </div>
  </body>
</html>

Este patron tiene el mismo defecto que los frames antiguos: la URL de la barra de direcciones no cambia, asi que el usuario no puede compartir ni marcar lo que esta viendo. Sirve para herramientas internas o visores de previsualizacion, no para la estructura de un sitio publico.

Seguridad: sandbox

Un iframe carga codigo que tu no escribiste, ejecutandose en el navegador de tu usuario. El atributo sandbox te deja apretar las tuercas.

La regla es contraintuitiva: poner sandbox sin valor aplica todas las restricciones a la vez. Luego vas devolviendo permisos uno por uno con los tokens que incluyas.

<!-- Maxima restriccion: sin scripts, sin formularios,
     sin popups, tratado como origen unico. -->
<iframe src="contenido-externo.html" title="Contenido no confiable" sandbox></iframe>

<!-- Restringido, pero se le devuelve la capacidad de ejecutar scripts
     y de enviar formularios. -->
<iframe
  src="widget.html"
  title="Widget de terceros"
  sandbox="allow-scripts allow-forms">
</iframe>
Token de sandboxPermiso que devuelve
allow-scriptsEjecutar JavaScript
allow-formsEnviar formularios
allow-same-originTratar el contenido con su origen real en vez de un origen unico y opaco
allow-popupsAbrir ventanas o pestanas nuevas
allow-popups-to-escape-sandboxQue las ventanas abiertas no hereden las restricciones
allow-modalsUsar dialogos como alert, confirm y prompt
allow-downloadsIniciar descargas de archivos
allow-top-navigationNavegar la pestana completa
allow-top-navigation-by-user-activationNavegar la pestana solo tras un gesto del usuario
allow-presentationIniciar sesiones de presentacion
allow-pointer-lockCapturar el puntero del raton
allow-orientation-lockBloquear la orientacion de pantalla

Hay una combinacion que anula el proposito del sandbox: allow-scripts junto con allow-same-origin en contenido del mismo origen que tu sitio. Con ambos permisos, el codigo del iframe puede alcanzar el documento padre y, entre otras cosas, quitarse el propio atributo sandbox. Si el contenido no es de confianza, no combines esos dos tokens.

Seguridad: allow y la politica de permisos

Mientras sandbox controla capacidades del documento (scripts, formularios, navegacion), allow controla el acceso a capacidades del dispositivo y del navegador: camara, microfono, ubicacion, reproduccion automatica y demas.

<!-- Un mapa que puede pedir la ubicacion del usuario,
     pero no la camara ni el microfono. -->
<iframe
  src="https://ejemplo.cl/mapa"
  title="Mapa interactivo"
  allow="geolocation">
</iframe>

<!-- Una videollamada incrustada que necesita camara y microfono,
     pero solo si el origen es exactamente ese. -->
<iframe
  src="https://reunion.ejemplo.cl/sala/42"
  title="Sala de reunion"
  allow="camera 'src'; microphone 'src'; display-capture 'src'">
</iframe>

Nombres de capacidades habituales: accelerometer, autoplay, camera, clipboard-write, display-capture, encrypted-media, fullscreen, geolocation, gyroscope, microphone, midi, payment, picture-in-picture, web-share.

La regla practica es la de siempre en seguridad: concede lo minimo. Si el contenido incrustado no necesita la camara, no la pongas en allow “por si acaso”.

Seguridad: que tu sitio no sea incrustado

Todo lo anterior te protege de lo que incrustas. Falta el reverso: impedir que otros incrusten tu sitio dentro del suyo. Ese ataque se llama clickjacking: alguien carga tu pagina en un iframe transparente sobre su propia interfaz, y el usuario cree que pulsa un boton inocente cuando en realidad esta pulsando un boton de tu aplicacion.

La defensa no es HTML, son cabeceras HTTP que envia tu servidor:

Content-Security-Policy: frame-ancestors 'self' https://socio-confiable.cl

frame-ancestors declara quien puede incrustarte. Con 'none' nadie puede; con 'self' solo tu propio sitio. La cabecera antigua equivalente es:

X-Frame-Options: SAMEORIGIN

X-Frame-Options acepta DENY y SAMEORIGIN. Se mantiene por compatibilidad, pero frame-ancestors es la forma actual y la que gana cuando ambas estan presentes.

Rendimiento: loading="lazy"

Un iframe cuesta caro. Si tienes tres videos en un articulo largo y el usuario nunca baja hasta el tercero, no tiene sentido descargarlo.

<iframe
  src="https://www.youtube.com/embed/LeYLP5uerGQ"
  title="Video del capitulo"
  loading="lazy"
  width="560"
  height="315">
</iframe>

Con loading="lazy" el navegador retrasa la carga hasta que el marco se aproxima al area visible. Es una linea que puede recortar mucho la carga inicial de una pagina.

Para casos extremos existe el patron de “fachada”: muestras una imagen estatica con un boton de reproduccion y solo insertas el iframe real cuando el usuario pulsa. Con lo que sabes hasta ahora ya puedes escribirlo:

.fachada {
  position: relative;
  width: 100%;
  max-width: 720px;
  aspect-ratio: 16 / 9;
  border: 0;
  padding: 0;
  cursor: pointer;
  background-size: cover;
  background-position: center;
}

.fachada::after {
  content: "\25B6";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 3rem;
  color: #fff;
  background: rgba(0, 0, 0, 0.35);
}
<button
  class="fachada"
  id="reproductor"
  style="background-image: url('miniatura.jpg')"
  aria-label="Reproducir el video del capitulo 7">
</button>

<script>
  const boton = document.getElementById('reproductor');

  boton.addEventListener('click', function () {
    const marco = document.createElement('iframe');
    marco.src = 'https://www.youtube.com/embed/LeYLP5uerGQ?autoplay=1';
    marco.title = 'Video del capitulo 7';
    marco.allow = 'autoplay; encrypted-media; picture-in-picture';
    marco.allowFullscreen = true;
    marco.style.cssText = 'width:100%;aspect-ratio:16/9;border:0';

    // Reemplazamos el boton por el iframe real.
    boton.replaceWith(marco);
  });
</script>

Hasta que el usuario pulsa, la pagina no descarga ni un byte del servicio externo.

Comunicacion entre padre e iframe

Cuando el iframe carga contenido de otro origen, tu JavaScript no puede leer su DOM ni el suyo el tuyo. El unico puente autorizado es postMessage.

En el documento padre:

<script>
  const marco = document.getElementById('miMarco');

  // Enviar un mensaje al iframe. El segundo argumento es el
  // origen esperado: nunca uses '*' con datos sensibles.
  marco.addEventListener('load', function () {
    marco.contentWindow.postMessage(
      { tipo: 'tema', valor: 'oscuro' },
      'https://ejemplo.cl'
    );
  });

  // Recibir mensajes del iframe.
  window.addEventListener('message', function (evento) {
    // Verificar SIEMPRE el origen antes de confiar en los datos.
    if (evento.origin !== 'https://ejemplo.cl') return;
    console.log('El iframe dice:', evento.data);
  });
</script>

Y dentro del iframe:

<script>
  window.addEventListener('message', function (evento) {
    if (evento.origin !== 'https://tusitio.cl') return;
    if (evento.data.tipo === 'tema') {
      document.body.dataset.tema = evento.data.valor;
    }
  });

  // Avisar al padre de la altura real del contenido.
  window.parent.postMessage(
    { tipo: 'altura', valor: document.body.scrollHeight },
    'https://tusitio.cl'
  );
</script>

La verificacion de evento.origin no es opcional. Sin ella, cualquier pagina podria enviarte mensajes falsos y tu codigo los trataria como legitimos. Es el error de seguridad mas frecuente al usar postMessage.

Si el iframe carga contenido del mismo origen, el aislamiento no aplica y puedes acceder directamente a marco.contentDocument para leer y modificar su DOM. Pero incluso en ese caso, postMessage suele ser mejor diseno porque no acopla los dos documentos.

Parte 3: divisores de contenido

Pasamos del contenido incrustado a la estructura del propio documento. Un divisor es una etiqueta cuyo trabajo no es mostrar algo concreto, sino agrupar y separar. Sirven para dos audiencias distintas: el CSS, que necesita algo a lo que aplicar estilos, y las herramientas que interpretan la pagina (lectores de pantalla, buscadores, modos de lectura), que necesitan saber que parte es que.

<hr>: una separacion tematica

<hr> se traduce a menudo como “linea horizontal”, y ese nombre confunde. En HTML moderno significa ruptura tematica: el tema cambia. Que el navegador lo dibuje como una linea es solo el estilo por defecto.

<article>
  <h2>Primera parte: los mapas de imagen</h2>
  <p>Contenido sobre map y area.</p>

  <hr>

  <h2>Segunda parte: los iframes</h2>
  <p>Contenido sobre contextos anidados.</p>
</article>

Es un elemento vacio: no tiene etiqueta de cierre y no contiene nada.

Si lo que quieres es una linea decorativa que separe visualmente dos bloques sin implicar cambio de tema, la herramienta correcta es CSS:

.tarjeta + .tarjeta {
  border-top: 1px solid #333;
}

Y si de todas formas usas <hr>, puedes estilizarlo:

hr {
  border: 0;
  height: 2px;
  background: linear-gradient(to right, transparent, #6f6, transparent);
  margin: 3rem 0;
}

<div> y <span>: los contenedores sin significado

Estos dos son deliberadamente neutros: no dicen nada sobre el contenido, solo lo agrupan.

  • <div> es de bloque: ocupa todo el ancho disponible y empieza en una linea nueva.
  • <span> es en linea: ocupa solo lo que necesita y convive con el texto a su alrededor.
<div class="alerta">
  <p>Cuidado, piso resbaloso.</p>
  <p>Se recomienda usar calzado antideslizante.</p>
</div>

<p>
  El precio final es de
  <span class="destacado">$19.990</span>
  con impuestos incluidos.
</p>

La regla de uso es simple: usa <div> o <span> cuando necesitas un gancho para CSS o JavaScript y ninguna otra etiqueta describe mejor lo que hay dentro. Si el contenido es una lista, usa <ul>. Si es un parrafo, usa <p>. Si es la cabecera de la pagina, usa <header>. El <div> es el ultimo recurso, no el primero.

Los contenedores semanticos

Estos si dicen algo sobre el contenido que envuelven.

EtiquetaQue significaCuantos por documento
<header>Contenido introductorio de la pagina o de una seccion: titulo, logo, buscadorVarios, uno por seccion
<nav>Un bloque de enlaces de navegacion importanteVarios, cada uno etiquetado
<main>El contenido principal y unico del documentoSolo uno visible
<section>Un agrupamiento tematico, normalmente con su propio encabezadoVarios
<article>Una pieza de contenido con sentido completo por si solaVarios
<aside>Contenido relacionado pero tangencial: barra lateral, notas, publicidadVarios
<footer>Cierre de la pagina o de una seccion: creditos, contacto, licenciaVarios, uno por seccion

Hay reglas concretas que conviene memorizar:

  • <main> es unico y no debe estar dentro de <article>, <aside>, <footer>, <header> ni <nav>. Marca el contenido central, lo que quedaria si quitaras cabecera, menu y pie.
  • <header> no es <head>. <head> va antes que <body> y contiene metadatos invisibles; <header> es contenido visible.
  • <section> casi siempre lleva un encabezado. Una <section> sin h1 a h6 es senal de que en realidad querias un <div>.
  • <nav> se reserva para navegacion principal. No hace falta envolver cada grupo de dos enlaces. Si hay varios <nav>, distinguelos con aria-label.

Como decidir entre section, article y div

flowchart TD
    A["Necesito agrupar<br/>este contenido"] --> B{"Hay una etiqueta<br/>especifica que ya lo<br/>describa? (ul, table,<br/>figure, form...)"}
    B -- Si --> C["Usala.<br/>No envuelvas por envolver"]
    B -- No --> D{"El contenido tiene<br/>sentido completo si lo<br/>saco de la pagina y lo<br/>publico solo?"}
    D -- Si --> E["article<br/>(post, comentario,<br/>ficha de producto,<br/>tarjeta de noticia)"]
    D -- No --> F{"Es un bloque tematico<br/>con su propio<br/>encabezado?"}
    F -- Si --> G["section<br/>(capitulo, apartado,<br/>bloque de la landing)"]
    F -- No --> H{"Es solo un gancho<br/>para CSS o JavaScript?"}
    H -- Si --> I["div si es de bloque<br/>span si es en linea"]
    H -- No --> J["Probablemente no<br/>necesitas envolver nada"]

El caso que mas se discute es article dentro de section y viceversa. Ambos son validos y ambos ocurren:

  • Una <section> llamada “Ultimas noticias” que contiene varios <article>: correcto.
  • Un <article> de blog que internamente tiene <section> para “Introduccion”, “Desarrollo” y “Conclusion”: tambien correcto.

Esqueleto completo de una pagina

graph TD
    BODY["body"]
    BODY --> HEADER["header<br/>logo + titulo del sitio"]
    HEADER --> NAV["nav aria-label='Principal'<br/>menu de secciones"]
    BODY --> MAIN["main<br/>contenido unico de esta pagina"]
    MAIN --> ART["article<br/>el post completo"]
    ART --> AHEAD["header<br/>titulo del post + fecha"]
    ART --> S1["section<br/>Introduccion"]
    ART --> HR["hr<br/>ruptura tematica"]
    ART --> S2["section<br/>Desarrollo"]
    ART --> AFOOT["footer<br/>autor + etiquetas"]
    MAIN --> ASIDE["aside<br/>articulos relacionados"]
    BODY --> FOOTER["footer<br/>copyright + contacto"]

Y su traduccion a codigo, completo y funcional:

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Estructura semantica completa</title>
    <style>
      :root {
        --fondo: #12121a;
        --texto: #e6e6ef;
        --acento: #7dd3fc;
        --borde: #2a2a3a;
      }

      * { box-sizing: border-box; }

      body {
        margin: 0;
        font-family: system-ui, sans-serif;
        line-height: 1.6;
        background: var(--fondo);
        color: var(--texto);
      }

      header, main, footer { padding: 1.5rem; }
      body > header { border-bottom: 1px solid var(--borde); }

      nav ul {
        list-style: none;
        display: flex;
        gap: 1.25rem;
        padding: 0;
        margin: 0.75rem 0 0;
      }

      nav a { color: var(--acento); text-decoration: none; }
      nav a:hover, nav a:focus-visible { text-decoration: underline; }

      main {
        display: grid;
        grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
        gap: 2rem;
        max-width: 1100px;
        margin: 0 auto;
      }

      @media (max-width: 720px) {
        main { grid-template-columns: 1fr; }
      }

      article {
        border: 1px solid var(--borde);
        border-radius: 10px;
        padding: 1.5rem;
      }

      article > header { padding: 0 0 1rem; }

      article > footer {
        padding: 1rem 0 0;
        border-top: 1px solid var(--borde);
        font-size: 0.9rem;
      }

      aside {
        border-left: 3px solid var(--acento);
        padding-left: 1rem;
        font-size: 0.95rem;
      }

      hr { border: 0; height: 1px; background: var(--borde); margin: 2rem 0; }

      body > footer {
        border-top: 1px solid var(--borde);
        text-align: center;
        font-size: 0.9rem;
      }
    </style>
  </head>

  <body>
    <header>
      <h1>Super Club Net</h1>
      <p>Notas de un curso de desarrollo web</p>

      <nav aria-label="Navegacion principal">
        <ul>
          <li><a href="/tecnologias/superclubnet/00-indice/">Indice</a></li>
          <li><a href="/tecnologias/superclubnet/06-enlaces-uri-url-y-rutas/">Capitulo 6</a></li>
          <li><a href="/tecnologias/superclubnet/08-javascript-tipos-variables-y-strings/">Capitulo 8</a></li>
        </ul>
      </nav>
    </header>

    <main>
      <article>
        <header>
          <h2>Mapas de imagen, iframes y divisores</h2>
          <p><time datetime="2026-08-17">17 de agosto de 2026</time></p>
        </header>

        <section>
          <h3>Mapas de imagen</h3>
          <p>
            Un mapa convierte zonas de una imagen en enlaces
            independientes mediante map y area.
          </p>
        </section>

        <hr>

        <section>
          <h3>Iframes</h3>
          <p>
            Un iframe crea un contexto de navegacion anidado con su
            propio documento, aislado del documento padre.
          </p>
        </section>

        <hr>

        <section>
          <h3>Divisores</h3>
          <p>
            Los contenedores semanticos describen el rol de cada bloque
            en la pagina, no solo su apariencia.
          </p>
        </section>

        <footer>
          <p>Escrito por Artiko. Etiquetas: html, semantica, accesibilidad.</p>
        </footer>
      </article>

      <aside aria-label="Contenido relacionado">
        <h2>Tambien te puede servir</h2>
        <ul>
          <li><a href="/tecnologias/superclubnet/06-enlaces-uri-url-y-rutas/">URL y rutas</a></li>
          <li><a href="/tecnologias/superclubnet/00-indice/">Todos los capitulos</a></li>
        </ul>
      </aside>
    </main>

    <footer>
      <p>Contenido del curso Super Club Net. Todos los capitulos en el indice.</p>
    </footer>
  </body>
</html>

Ese documento no usa ni un solo <div>, y aun asi tiene una estructura completa con cabecera, menu, contenido principal, barra lateral y pie. No es una regla que haya que llevar al extremo, pero muestra hasta donde llega el vocabulario semantico antes de tener que recurrir a contenedores neutros.

Que ganas con la semantica

No es una cuestion estetica. Tiene efectos medibles:

BeneficioComo se manifiesta
Navegacion por regionesUn lector de pantalla permite saltar directo a “principal”, “navegacion” o “pie”
Modo lecturaFirefox, Safari y otros detectan <article> y <main> para extraer el contenido limpio
BuscadoresDistinguen el contenido central del menu repetido en cada pagina
MantenimientoLeer el HTML de otra persona es mucho mas rapido cuando las etiquetas dicen que hacen
CSS mas limpioPuedes escribir main article > header sin inventar tres clases

Contenedores especializados que conviene conocer

Ademas de los generales, hay contenedores para casos especificos que resuelven problemas concretos:

<!-- Imagen o diagrama con su leyenda asociada -->
<figure>
  <img src="diagrama-iframe.png" alt="Esquema de un contexto de navegacion anidado">
  <figcaption>El iframe crea un documento independiente dentro del padre.</figcaption>
</figure>

<!-- Bloque plegable, sin JavaScript -->
<details>
  <summary>Ver los tokens de sandbox menos usados</summary>
  <p>allow-orientation-lock, allow-pointer-lock y allow-presentation.</p>
</details>

<!-- Cita de otra fuente, con su origen -->
<blockquote cite="https://developer.mozilla.org/es/docs/Web/HTML/Element/iframe">
  <p>El elemento iframe representa un contexto de navegacion anidado.</p>
</blockquote>

<details> y <summary> merecen atencion: dan un comportamiento de acordeon completamente funcional, con soporte de teclado y accesibilidad incluidos, sin escribir una linea de JavaScript.

Tabla comparativa general del capitulo

ElementoCategoriaEs vacioRol principalCuidado principal
<map>Metadatos de imagenNoAgrupar zonas con un nombreEl name debe coincidir exacto con el usemap
<area>Enlace geometricoSiDefinir una zona pulsableEl alt es obligatorio si hay href
<iframe>Contenido incrustadoNoCargar un documento anidadoCoste de rendimiento y superficie de seguridad
<hr>FlujoSiMarcar un cambio de temaNo usarlo como decoracion
<div>Flujo genericoNoAgrupar en bloqueSin significado: ultimo recurso
<span>Flujo en lineaNoAgrupar dentro del textoSin significado: ultimo recurso
<header>SeccionadoNoIntroduccion de una seccionNo confundir con <head>
<nav>SeccionadoNoNavegacion importanteEtiquetar si hay varios
<main>SeccionadoNoContenido central unicoSolo uno, y no anidado en otros
<section>SeccionadoNoBloque tematicoDebe llevar encabezado
<article>SeccionadoNoPieza autonomaSolo si tiene sentido fuera de contexto
<aside>SeccionadoNoContenido tangencialNo para cualquier columna lateral decorativa
<footer>SeccionadoNoCierre de una seccionPuede haber varios

Errores comunes

ErrorCausaSolucion
El mapa de imagen no responde a ningun clicEl usemap no coincide con el name del <map>, o falta la almohadillaVerifica que sea usemap="#nombre" y <map name="nombre">, con el mismo texto exacto y respetando mayusculas
Toda la imagen lleva al mismo destinoEl <area shape="default"> esta escrito antes que los demasMueve la zona default al final del <map>: gana la primera coincidencia
Las zonas estan desplazadas en el movilEl navegador no reescala las coordenadas cuando la imagen se muestra a otro tamanoRecalcula coords con JavaScript o cambia a enlaces posicionados en porcentajes
El validador marca el HTML como invalido en <area>Hay un <area> con href pero sin altAnade un alt descriptivo, o quita el href si la zona no debe ser un enlace
poly no dibuja la zona esperadaSe repitio el primer vertice al final o hay un numero impar de valoresElimina el punto repetido y comprueba que coords tenga pares completos
El iframe se ve en blanco y la consola muestra un error de frame-ancestorsEl sitio de destino prohibe ser incrustado mediante CSP o X-Frame-OptionsNo hay solucion desde tu lado: usa la URL de incrustacion oficial del servicio o enlaza en vez de incrustar
El video de YouTube muestra un error dentro del iframeSe uso la URL de la pagina (watch?v=) en vez de la de incrustacionUsa https://www.youtube.com/embed/ID
Mi CSS no afecta al contenido del iframeEl iframe es un documento independiente con su propio arbol de estilosSolo puedes estilizar la caja exterior; el interior lo controla el documento incrustado
contentDocument devuelve nullEl iframe carga otro origen y la politica de mismo origen lo bloqueaUsa postMessage para comunicarte, verificando siempre evento.origin
El sandbox no restringe nadaSe combino allow-scripts con allow-same-origin en contenido del mismo origenQuita uno de los dos tokens si el contenido no es de confianza
La pagina tarda mucho en cargar con varios videosCada iframe descarga un documento completo desde el inicioAnade loading="lazy" o usa el patron de fachada con miniatura
El iframe se desborda en pantallas pequenaswidth y height fijos en pixelesUsa width: 100% con aspect-ratio en CSS
Un lector de pantalla anuncia solo “marco”Falta el atributo title en el <iframe>Anade un title que describa el contenido incrustado
El validador se queja de dos <main>Hay mas de un <main> visible en el documentoDeja uno solo, o marca los demas con el atributo hidden
<section> sin encabezado genera advertencias de accesibilidadSe uso <section> como si fuera <div>Anade un encabezado, o cambia a <div> si el bloque no es tematico
El <hr> aparece donde no se queria una lineaSe uso <hr> como separador decorativoUsa border-top en CSS y reserva <hr> para cambios de tema
<header> no muestra nada en la parte superiorSe confundio con <head> y se puso fuera del <body><header> va dentro de <body>; <head> va antes y contiene metadatos

Ejercicios propuestos

1. Panel de coordenadas. Toma una foto tuya o cualquier imagen con al menos tres elementos distinguibles. Usando el medidor de coordenadas de este capitulo, anota las coordenadas de cada elemento y construye un <map> con un rect, un circle y un poly. Anade un default que lleve a una pagina de ayuda.

2. Mapa que sobrevive al movil. Toma el mapa del ejercicio 1 y hazlo adaptable de dos maneras distintas: primero con el script de reescalado y ResizeObserver, y despues reescribiendolo con enlaces posicionados en porcentajes. Compara ambos resultados reduciendo la ventana del navegador.

3. Zona muerta. Anade al mapa del ejercicio 1 una region central sin href que impida que el clic caiga en la zona default. Verifica que efectivamente no pasa nada al pulsar ahi.

4. Visor con menu lateral. Construye la pagina con <nav> y un <iframe name="visor">. Anade cuatro enlaces con target="visor". Despues anade un quinto enlace con target="_top" y observa la diferencia de comportamiento.

5. Escalera de sandbox. Crea un archivo travieso.html que intente tres cosas: ejecutar un alert, enviar un formulario y abrir una ventana nueva. Incrustalo cuatro veces en una pagina: sin sandbox, con sandbox vacio, con sandbox="allow-scripts" y con sandbox="allow-scripts allow-modals allow-popups". Documenta en una tabla que funciona en cada caso.

6. Fachada de video. Implementa el patron de fachada. Abre la pestana de red de las herramientas de desarrollo y compara el numero de peticiones antes y despues de pulsar el boton.

7. Puente postMessage. Crea dos archivos en el mismo directorio: padre.html con un iframe y un campo de texto, e hijo.html que reciba lo escrito y lo muestre en su cuerpo. Anade la verificacion de evento.origin en ambos lados y comprueba que el mensaje se descarta si cambias el origen esperado.

8. Deconstruccion semantica. Elige una pagina de un sitio real que uses a diario. Dibuja en papel su estructura y despues escribe el esqueleto HTML equivalente usando solo header, nav, main, section, article, aside y footer. Anota cuantas veces necesitaste recurrir a un <div> y por que.

9. Auditoria de div. Toma cualquier pagina que hayas escrito en capitulos anteriores y cuenta sus <div>. Reemplaza cada uno por la etiqueta semantica correcta cuando exista. Justifica cada superviviente.

10. Todo junto. Construye una pagina de una sola vista que combine los tres temas: un mapa de imagen adaptable como cabecera visual, un <iframe> con loading="lazy" en el contenido principal, y una estructura semantica completa con header, nav, main, article, aside y footer. Pasala por el validador de HTML del W3C hasta que no reporte errores.

Lo que viene

Con este capitulo cerramos el recorrido por HTML. Ya sabes describir texto, estructurar tablas, insertar imagenes y multimedia, construir enlaces con rutas correctas, convertir imagenes en superficies interactivas, incrustar documentos ajenos controlando lo que pueden hacer, y organizar todo eso en una estructura que tanto una persona como una maquina pueden entender.

Hasta aqui, sin embargo, todo ha sido estatico: el documento se describe una vez y el navegador lo pinta. Las unicas piezas moviles que aparecieron fueron pequenos scripts que usamos sin explicar de verdad: el medidor de coordenadas, el reescalado del mapa, la fachada del video, el puente de postMessage.

Es hora de entender ese lenguaje desde la base. En el capitulo 8 empezamos con JavaScript: como se incluye en la pagina, que tipos de datos existen, como se declaran variables con let y const, que diferencia hay entre valores primitivos y objetos, y como trabajar con cadenas de texto en profundidad. Ese es el punto en el que la pagina deja de ser un documento y empieza a ser un programa.

El recorrido completo del curso esta en el indice.