Enlaces, URIs y rutas: la etiqueta a de punta a punta
Enlaces, URIs y rutas: la etiqueta a de punta a punta
En el capítulo 5 trabajaste con <img>, con background-image y, sobre todo, con la idea de ruta: ese texto que va dentro de src o dentro de url() y que el navegador convierte en una petición concreta a un servidor. Ahí las rutas apuntaban a recursos que el navegador incrustaba dentro de la página sin cambiar de documento.
Este capítulo toma esa misma idea de ruta y la lleva al otro extremo: en vez de traer un recurso hacia el documento actual, vamos a mover al usuario hacia otro documento. Eso es un hipervínculo, y es literalmente la razón por la que la web se llama web. Sin la etiqueta <a> tendrías una colección de archivos HTML sueltos; con ella tienes una red navegable.
Vamos a cubrir el hipervínculo en profundidad: la etiqueta y todos sus atributos útiles, los estados CSS de un enlace y el orden en que deben escribirse, la anatomía completa de una URI según su especificación, la diferencia real entre URI, URL y URN, cómo el navegador resuelve una ruta relativa, por qué index.html es especial, cómo funcionan los enlaces con # y cómo mailto: y tel: salen del navegador para hablarle al sistema operativo.
La etiqueta a: anatomía del hipervínculo
La etiqueta <a> (de anchor, ancla) crea un hipervínculo. Su forma mínima es:
<a href="pagina.html">Texto visible del enlace</a>
Tiene tres partes que conviene nombrar por separado, porque cada una cumple una función distinta:
- La etiqueta de apertura con el atributo
href: define el destino.hrefsignifica hypertext reference. - El contenido: lo que el usuario ve y sobre lo que hace clic. Puede ser texto, una imagen, o incluso varios elementos.
- La etiqueta de cierre:
</a>.
Un documento completo con enlaces:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Super Club Net - Enlaces</title>
</head>
<body>
<h1>Super Club Net</h1>
<p>
Visita nuestra <a href="nosotros.html">página de nosotros</a>
o revisa la <a href="https://developer.mozilla.org/es/">documentación de MDN</a>.
</p>
<p>También puedes ver el <a href="documentos/reglamento.pdf">reglamento del club</a>.</p>
</body>
</html>
Si no le das estilos, el navegador aplica su hoja de estilos por defecto: el texto aparece subrayado y en azul, y el cursor cambia a una mano al pasar por encima. Esos dos indicadores no son decoración, son el contrato visual que le dice al usuario “esto se puede clickear”.
El contenido del enlace no tiene que ser texto
Un <a> puede envolver cualquier contenido en línea y, en HTML5, también bloques completos. Esto es perfectamente válido:
<!-- Una imagen que funciona como enlace -->
<a href="galeria.html">
<img src="imagenes/miniatura-galeria.jpg" alt="Ver la galería del club">
</a>
<!-- Una tarjeta completa como enlace -->
<a href="torneos/2026-verano.html" class="tarjeta">
<h2>Torneo de Verano 2026</h2>
<p>Inscripciones abiertas hasta el 15 de enero.</p>
</a>
Cuando envuelves una imagen, el alt de la imagen pasa a ser el texto accesible del enlace. Por eso ese alt debe describir el destino, no la imagen: “Ver la galería del club” es mejor que “foto de una cancha”.
Lo que no puedes hacer es anidar un <a> dentro de otro <a>, ni meter un <button> dentro de un <a>. El navegador corrige el HTML inválido de formas impredecibles y el resultado casi nunca es el que esperabas.
Cuándo usar a y cuándo usar button
Esta distinción confunde a mucha gente y vale la pena fijarla ahora:
| Elemento | Se usa para | Qué hace el navegador |
|---|---|---|
<a href="..."> | Navegar a otro recurso o a otra parte del documento | Cambia la URL, agrega una entrada al historial, permite abrir en pestaña nueva, permite copiar la dirección |
<button> | Ejecutar una acción en la página actual | No cambia la URL ni el historial; dispara JavaScript o envía un formulario |
Reglas prácticas:
- Si el usuario debería poder hacer clic derecho y elegir “Abrir en una pestaña nueva”, es un
<a>. - Si al hacer clic se abre un modal, se agrega un producto al carrito o se despliega un menú, es un
<button>. - Nunca uses
<a href="#">con JavaScript para simular un botón: rompe el historial, ensucia la URL con un#y confunde a los lectores de pantalla, que anunciarán “enlace” cuando en realidad es un control.
<!-- Incorrecto: un botón disfrazado de enlace -->
<a href="#" onclick="abrirMenu()">Menú</a>
<!-- Correcto -->
<button type="button" onclick="abrirMenu()">Menú</button>
Los estados de un enlace en CSS
Un enlace no es un elemento estático: pasa por distintos estados según lo que haga el usuario y según el historial del navegador. CSS expone esos estados como pseudoclases.
stateDiagram-v2
[*] --> SinVisitar: el usuario nunca abrio este href
SinVisitar --> Hover: el puntero entra al area del enlace
Hover --> SinVisitar: el puntero sale
Hover --> Active: el usuario presiona el boton del mouse
Active --> Visitado: se suelta el boton y ocurre la navegacion
SinVisitar --> Focus: el usuario llega con la tecla Tab
Focus --> Active: el usuario presiona Enter o Espacio
Visitado --> Hover: vuelve a pasar el puntero
Visitado --> [*]
note right of SinVisitar
a:link
aplica solo si el href existe
end note
note right of Visitado
a:visited
el navegador limita
que propiedades se pueden
cambiar aqui por privacidad
end note
note right of Focus
a:focus y a:focus-visible
imprescindibles para teclado
end note
Las cinco pseudoclases relevantes:
| Pseudoclase | Cuándo aplica |
|---|---|
a:link | El enlace tiene href y el usuario no lo ha visitado |
a:visited | El usuario ya visitó ese destino según el historial del navegador |
a:hover | El puntero está encima del enlace |
a:focus | El enlace tiene el foco del teclado (llegaste con Tab) |
a:active | El enlace está siendo presionado en este instante |
El orden LVHA no es opcional
Estas pseudoclases tienen la misma especificidad, así que si dos coinciden, gana la que se declaró después. Eso obliga a escribirlas en un orden concreto: :link, :visited, :focus, :hover, :active. La regla mnemotécnica clásica es LVHA (“LoVe HAte”), con :focus insertado antes de :hover.
Si pones :hover antes que :visited, un enlace ya visitado nunca cambiará de color al pasar el mouse, porque :visited lo sobrescribe.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Estados de un enlace</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; }
/* 1 */ a:link { color: #1c5d99; text-decoration: underline; }
/* 2 */ a:visited { color: #6b3fa0; }
/* 3 */ a:focus-visible { outline: 3px solid #ffb703; outline-offset: 2px; }
/* 4 */ a:hover { color: #d62828; text-decoration-thickness: 2px; }
/* 5 */ a:active { color: #9d0208; }
</style>
</head>
<body>
<h1>Prueba los estados</h1>
<ul>
<li><a href="https://example.com/uno">Enlace probablemente no visitado</a></li>
<li><a href="https://developer.mozilla.org/">Enlace probablemente visitado</a></li>
<li><a href="#final">Enlace interno</a></li>
</ul>
<p id="final">Presiona Tab para ver el estado de foco.</p>
</body>
</html>
Dos advertencias importantes
Sobre :visited. Por privacidad, los navegadores limitan severamente qué propiedades puedes cambiar en :visited. Básicamente solo colores (color, background-color, border-color, outline-color). Además, getComputedStyle() desde JavaScript miente sobre esos valores. Esto existe porque, sin la restricción, una página podría medir el estilo de cientos de enlaces y deducir tu historial de navegación completo.
Sobre text-decoration: none. Quitar el subrayado es tentador y muy común, pero deja al color como único indicador de que algo es un enlace. Alrededor del 8 % de los hombres tiene alguna forma de daltonismo, y para muchos de ellos un enlace azul dentro de texto negro es indistinguible. Si quitas el subrayado dentro de un párrafo, agrega otro indicador: negrita, un borde inferior, un fondo sutil. En una barra de navegación, donde el contexto ya deja claro que todo es navegable, quitar el subrayado sí es razonable.
/* Aceptable: navegación, el contexto ya comunica */
nav a {
text-decoration: none;
}
/* Riesgoso: enlaces dentro de un párrafo sin ningún otro indicador */
p a {
text-decoration: none;
}
/* Mejor: sin subrayado pero con otro indicador */
p a {
text-decoration: none;
border-bottom: 2px solid currentColor;
}
URI, URL y URN: los nombres correctos
El valor de href es una URI. Vale la pena aclarar la terminología porque se usa mal todo el tiempo.
- URI (Uniform Resource Identifier, identificador uniforme de recursos): una cadena de texto que identifica un recurso. Es el término general.
- URL (Uniform Resource Locator, localizador uniforme de recursos): una URI que además dice cómo y dónde encontrar el recurso.
https://ninjas.cl/libros/es una URL: indica el protocolo y la ubicación. - URN (Uniform Resource Name, nombre uniforme de recursos): una URI que nombra un recurso de forma persistente sin decir dónde está.
urn:isbn:9780134685991identifica un libro concreto, pero no te dice en qué servidor descargarlo.
flowchart TD
URI["URI<br/>Identificador uniforme de recursos<br/>RFC 3986"]
URL["URL<br/>Dice DONDE y COMO<br/>obtener el recurso"]
URN["URN<br/>Dice COMO SE LLAMA<br/>el recurso, no donde esta"]
URI --> URL
URI --> URN
URL --> E1["https://ninjas.cl/libros/"]
URL --> E2["ftp://ftp.is.co.za/rfc/rfc1808.txt"]
URL --> E3["mailto:[email protected]"]
URN --> E4["urn:isbn:0451450523"]
URN --> E5["urn:ietf:rfc:3986"]
style URI fill:#1c5d99,color:#ffffff
style URL fill:#2a9d8f,color:#ffffff
style URN fill:#e76f51,color:#ffffff
En la práctica diaria casi todo lo que escribes en un href es una URL, y el mundo usa “URL” y “URI” como sinónimos. No es grave, pero saber la diferencia te ayuda a leer especificaciones sin perderte.
La sintaxis genérica de una URI
El documento que define esto es el RFC 3986. Su sintaxis genérica es:
esquema:[//autoridad]ruta[?consulta][#fragmento]
Y la autoridad, cuando existe, se descompone a su vez:
autoridad = [usuario[:contraseña]@]host[:puerto]
Veamos una URL completa desmenuzada:
https://blog.ejemplo.cl:8443/articulos/2026/enlaces?orden=fecha&pagina=2#comentarios
flowchart LR
subgraph URL["Una URL completa, componente por componente"]
direction LR
A["esquema<br/>https"]
B["host<br/>blog.ejemplo.cl"]
C["puerto<br/>8443"]
D["ruta<br/>/articulos/2026/enlaces"]
E["consulta<br/>orden=fecha y pagina=2"]
F["fragmento<br/>comentarios"]
end
A --> B --> C --> D --> E --> F
A -.-> A1["Que protocolo usar"]
B -.-> B1["A que servidor conectarse<br/>se resuelve por DNS"]
C -.-> C1["En que puerto TCP<br/>se omite si es el estandar"]
D -.-> D1["Que recurso pedir<br/>dentro de ese servidor"]
E -.-> E1["Parametros para el servidor<br/>SI viaja en la peticion"]
F -.-> F1["A que parte del documento ir<br/>NO viaja al servidor"]
style A fill:#1c5d99,color:#ffffff
style B fill:#2a9d8f,color:#ffffff
style C fill:#8ab17d,color:#000000
style D fill:#e9c46a,color:#000000
style E fill:#f4a261,color:#000000
style F fill:#e76f51,color:#ffffff
Tabla de referencia de componentes
| Componente | Delimitador | Obligatorio | Ejemplo | Notas |
|---|---|---|---|---|
| Esquema | seguido de : | Sí | https | Siempre en minúsculas por convención; define el protocolo |
| Autoridad | precedida por // | No | blog.ejemplo.cl:8443 | mailto: y urn: no la tienen |
| Usuario e info | seguido de @ | No | admin@ | Desaconsejado en HTTP; los navegadores lo bloquean o advierten |
| Host | — | Si hay autoridad | blog.ejemplo.cl | Puede ser un nombre de dominio o una IP |
| Puerto | precedido por : | No | :8443 | Se omite cuando coincide con el puerto por defecto del esquema |
| Ruta | empieza con / si hay autoridad | Sí (puede ser vacía) | /articulos/2026/enlaces | Sensible a mayúsculas en la mayoría de los servidores |
| Consulta | precedida por ? | No | orden=fecha&pagina=2 | Pares clave=valor separados por & |
| Fragmento | precedido por # | No | comentarios | Lo procesa solo el navegador, nunca se envía al servidor |
Ese último punto es de los más importantes del capítulo y volveremos a él: el fragmento no viaja en la petición HTTP. El servidor jamás se entera de qué había después del #.
Ejemplos de distintos esquemas
https://ninjas.cl/books/superclubnet/
http://localhost:4321/tecnologias/
ftp://ftp.is.co.za/rfc/rfc1808.txt
ldap://[2001:db8::7]/c=GB?objectClass?one
mailto:[email protected]
tel:+56912345678
file:///home/usuario/proyectos/index.html
data:text/plain;charset=utf-8,Hola%20mundo
Fíjate en tres detalles:
- La dirección IPv6 va entre corchetes (
[2001:db8::7]) para que los:de la IP no se confundan con el:del puerto. mailto:ytel:no llevan//porque no tienen autoridad: no hay un servidor al cual conectarse.file:///tiene tres barras porque la autoridad está vacía: esfile://+/home/....
Anatomía del nombre de dominio
El host de una URL suele ser un nombre de dominio, y ese nombre también tiene estructura. Se lee de derecha a izquierda, de lo más general a lo más específico:
flowchart RL
TLD["cl<br/>TLD<br/>dominio de primer nivel"]
SLD["ejemplo<br/>SLD<br/>dominio de segundo nivel"]
SUB1["tienda<br/>subdominio"]
SUB2["blog<br/>subdominio"]
TLD --> SLD --> SUB1 --> SUB2
TLD -.-> N1["Lo administra un registro<br/>NIC Chile en este caso"]
SLD -.-> N2["Es lo que se registra y se paga"]
SUB1 -.-> N3["Los crea libremente<br/>el dueno del dominio"]
SUB2 -.-> N3
style TLD fill:#1c5d99,color:#ffffff
style SLD fill:#2a9d8f,color:#ffffff
style SUB1 fill:#e9c46a,color:#000000
style SUB2 fill:#f4a261,color:#000000
El nombre completo blog.tienda.ejemplo.cl se lee de derecha a izquierda: cl es el dominio de primer nivel, ejemplo el de segundo nivel, y tienda y blog son subdominios encadenados.
| Parte | Nombre | Ejemplos | Quién lo controla |
|---|---|---|---|
.cl, .com, .net, .org | TLD, dominio de primer nivel | .cl para Chile, .com genérico | Registros globales o nacionales (NIC Chile para .cl) |
ejemplo | SLD, dominio de segundo nivel | google, ninjas, wikipedia | Quien lo registra y paga por él |
blog, www, api | Subdominio | www.ejemplo.cl, api.ejemplo.cl | El dueño del dominio, libremente |
www es un subdominio como cualquier otro; no tiene nada de mágico. Es una convención heredada de los noventa, cuando una organización tenía un servidor para web (www), otro para correo (mail) y otro para archivos (ftp).
Sobre los puertos
Un servidor puede tener muchos servicios escuchando al mismo tiempo. El puerto es el número que dice a cuál te quieres conectar.
| Esquema | Puerto por defecto | Se escribe en la URL |
|---|---|---|
http | 80 | Solo si es distinto de 80 |
https | 443 | Solo si es distinto de 443 |
ftp | 21 | Solo si es distinto de 21 |
Por eso https://ejemplo.cl y https://ejemplo.cl:443 son exactamente la misma dirección. En desarrollo local sí verás puertos explícitos todo el tiempo, porque los servidores de desarrollo escuchan en puertos altos: http://localhost:4321, http://localhost:3000, http://localhost:8080.
http contra https
http transmite todo en texto plano: cualquiera en la misma red puede leer lo que envías. https es el mismo protocolo pero dentro de un túnel cifrado con TLS. Hoy no hay razón para servir un sitio público por http: los certificados son gratuitos y los navegadores marcan como “no seguro” cualquier sitio que no use https.
Rutas absolutas y relativas en href
En el capítulo anterior viste rutas para imágenes. Aquí aplican exactamente las mismas reglas, así que este es un buen momento para formalizarlas.
Supongamos esta estructura de archivos:
sitio/
index.html
nosotros.html
css/
estilos.css
imagenes/
logo.png
torneos/
index.html
2026-verano.html
resultados/
index.html
final.html
Si estás editando torneos/resultados/final.html, estas son las formas de enlazar:
| Forma escrita | Tipo | Resuelve a | Cuándo conviene |
|---|---|---|---|
final.html | Relativa a la carpeta actual | /torneos/resultados/final.html | Archivos hermanos |
./final.html | Relativa explícita | /torneos/resultados/final.html | Igual a la anterior, más explícita |
../2026-verano.html | Sube un nivel | /torneos/2026-verano.html | Subir en la jerarquía |
../../nosotros.html | Sube dos niveles | /nosotros.html | Volver a la raíz desde profundo |
/nosotros.html | Absoluta de raíz | /nosotros.html | Sitios servidos desde la raíz de un dominio |
https://ejemplo.cl/a.html | Absoluta completa | Ese dominio exacto | Sitios externos |
//ejemplo.cl/a.html | Relativa de protocolo | Hereda http o https de la página actual | Prácticamente obsoleta, usa https:// |
flowchart TD
START["href encontrado en el HTML"] --> Q1{"Empieza con<br/>esquema seguido de dos puntos?"}
Q1 -->|"Si: https:, mailto:, tel:"| ABS["URL absoluta<br/>se usa tal cual"]
Q1 -->|No| Q2{"Empieza con //?"}
Q2 -->|Si| PROTO["Hereda el esquema<br/>de la pagina actual"]
Q2 -->|No| Q3{"Empieza con /?"}
Q3 -->|Si| ROOT["Se pega despues de<br/>esquema mas host mas puerto<br/>de la pagina actual"]
Q3 -->|No| Q4{"Empieza con numeral?"}
Q4 -->|Si| FRAG["Misma pagina<br/>solo cambia el fragmento<br/>no hay peticion HTTP"]
Q4 -->|No| Q5{"Empieza con signo de interrogacion?"}
Q5 -->|Si| QRY["Misma ruta<br/>reemplaza la consulta"]
Q5 -->|No| REL["Ruta relativa<br/>se resuelve contra la carpeta<br/>del documento actual<br/>o contra la etiqueta base"]
ABS --> FINAL["URL final resuelta"]
PROTO --> FINAL
ROOT --> FINAL
FRAG --> FINAL
QRY --> FINAL
REL --> FINAL
style START fill:#1c5d99,color:#ffffff
style FINAL fill:#2a9d8f,color:#ffffff
style FRAG fill:#e76f51,color:#ffffff
Ejemplo ejecutable de resolución de rutas
Crea esta estructura y ábrela con un servidor local para ver la resolución en acción.
torneos/resultados/final.html:
<head>
<meta charset="utf-8">
<title>Final del torneo</title>
<link rel="stylesheet" href="../../css/estilos.css">
</head>
<body>
<h1>Final del Torneo 2026</h1>
<nav>
<ul>
<li><a href="index.html">Todos los resultados</a></li>
<li><a href="../2026-verano.html">Ficha del torneo</a></li>
<li><a href="../../nosotros.html">Sobre el club</a></li>
<li><a href="/">Portada</a></li>
<li><a href="https://ninjas.cl/">Sitio externo</a></li>
</ul>
</nav>
<p>Consulta el <a href="../../documentos/acta-final.pdf">acta oficial en PDF</a>.</p>
</body>
Una precisión sobre la ruta absoluta de raíz (/nosotros.html): la barra inicial significa “desde la raíz del dominio”, no “desde la raíz de mi proyecto”. Si publicas el sitio en https://ejemplo.cl/superclubnet/, entonces /nosotros.html apuntará a https://ejemplo.cl/nosotros.html y dará 404. En ese escenario las rutas relativas son más portables, o bien necesitas la etiqueta <base>.
La etiqueta base
<base> va en el <head> y cambia el punto de referencia para todas las rutas relativas del documento.
<head>
<meta charset="utf-8">
<base href="https://ejemplo.cl/superclubnet/">
<title>Con base</title>
</head>
<body>
<!-- Esto resuelve a https://ejemplo.cl/superclubnet/nosotros.html -->
<a href="nosotros.html">Nosotros</a>
</body>
Solo puede haber una etiqueta <base> por documento y debe aparecer antes de cualquier otro elemento que use una ruta. Es una herramienta poderosa y peligrosa: afecta a imágenes, hojas de estilo, formularios y enlaces al mismo tiempo, incluidos los enlaces con #. Úsala con cuidado.
index.html: el documento por defecto
Cuando escribes https://ejemplo.cl/torneos/ estás pidiendo un directorio, no un archivo. El servidor tiene tres opciones: devolver un error, listar el contenido del directorio, o devolver un archivo por defecto. Casi todos los servidores están configurados para lo tercero, y ese archivo por defecto es index.html.
sequenceDiagram
autonumber
participant U as Usuario
participant N as Navegador
participant D as DNS
participant S as Servidor web
U->>N: Escribe https://ejemplo.cl/torneos/
N->>D: Que IP tiene ejemplo.cl?
D-->>N: 203.0.113.42
N->>S: GET /torneos/ HTTP/1.1<br/>Host: ejemplo.cl
Note over S: La ruta termina en /<br/>se pide un directorio
S->>S: Busca el documento por defecto<br/>configurado: index.html
alt Existe /torneos/index.html
S-->>N: 200 OK con el contenido de index.html
N-->>U: Renderiza la pagina
else No existe y el listado esta activado
S-->>N: 200 OK con el listado del directorio
N-->>U: Muestra la lista de archivos
else No existe y el listado esta desactivado
S-->>N: 403 Forbidden o 404 Not Found
N-->>U: Muestra la pagina de error
end
Consecuencias prácticas:
https://ejemplo.cl/yhttps://ejemplo.cl/index.htmlsirven el mismo contenido, pero son URLs distintas para efectos de caché, historial y buscadores. Elige una forma y sé consistente; lo habitual es enlazar siempre a la versión sinindex.html.- Escribe el nombre todo en minúsculas. En Linux, que es donde corre la mayoría de los servidores,
Index.htmleindex.htmlson archivos diferentes. En Windows y macOS suelen no distinguirse, y ese es justamente el problema: funciona en tu máquina y falla al publicar. - Cada carpeta puede tener su propio
index.html. Es la forma clásica de construir URLs limpias sin ninguna configuración especial.
sitio/
index.html -> /
torneos/
index.html -> /torneos/
2026-verano.html -> /torneos/2026-verano.html
contacto/
index.html -> /contacto/
Algunos servidores aceptan otros nombres (index.htm, default.html, index.php) y el orden de preferencia se configura en el servidor. Si trabajas con un servidor propio, index.html es la apuesta segura.
La barra final importa
/torneos y /torneos/ no son lo mismo, y la diferencia se nota al resolver rutas relativas:
| Página actual | href="ficha.html" resuelve a |
|---|---|
https://ejemplo.cl/torneos/ | https://ejemplo.cl/torneos/ficha.html |
https://ejemplo.cl/torneos | https://ejemplo.cl/ficha.html |
En el segundo caso el navegador considera que torneos es un archivo, no una carpeta, así que lo descarta y sube un nivel. La mayoría de los servidores emiten una redirección 301 de /torneos a /torneos/ justamente para evitar este problema, pero no todos.
Enlaces con hashtag: fragmentos dentro del documento
Un href que empieza con # no pide nada al servidor. El navegador busca dentro del documento actual un elemento cuyo atributo id coincida con el texto que sigue al #, y hace scroll hasta él.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Reglamento del Super Club Net</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 45rem; margin: 2rem auto; }
section { min-height: 70vh; padding-top: 1rem; }
</style>
</head>
<body>
<h1 id="inicio">Reglamento del Super Club Net</h1>
<nav aria-label="Índice del reglamento">
<ol>
<li><a href="#socios">De los socios</a></li>
<li><a href="#cuotas">De las cuotas</a></li>
<li><a href="#torneos">De los torneos</a></li>
</ol>
</nav>
<section id="socios">
<h2>1. De los socios</h2>
<p>Puede ser socio cualquier persona mayor de 14 años.</p>
<p><a href="#inicio">Volver al índice</a></p>
</section>
<section id="cuotas">
<h2>2. De las cuotas</h2>
<p>La cuota mensual se paga dentro de los primeros cinco días.</p>
<p><a href="#inicio">Volver al índice</a></p>
</section>
<section id="torneos">
<h2>3. De los torneos</h2>
<p>Los torneos oficiales se organizan cada temporada.</p>
<p><a href="#inicio">Volver al índice</a></p>
</section>
</body>
</html>
Cada <section> tiene un id y cada enlace del índice apunta a ese id con un # delante. El min-height: 70vh está solo para que las secciones sean altas y el salto se note al probar el ejemplo. Ningún clic dentro de esta página genera una petición HTTP: todo el trabajo lo hace el navegador sobre el documento ya cargado.
Las tres formas del fragmento
href | Comportamiento |
|---|---|
#seccion | Salta al elemento con id="seccion" |
# | Salta al inicio del documento y agrega un # a la URL |
#top | Salta al inicio del documento aunque no exista un id="top" (comportamiento definido en la especificación de HTML) |
Un enlace href="#" funciona como “volver arriba”, pero ensucia la URL y agrega una entrada al historial. Si de verdad quieres un botón de “volver arriba”, enlaza a un id real del encabezado principal.
Reglas para los id
- Un
iddebe ser único en todo el documento. Si hay dos elementos con el mismoid, el navegador salta al primero y el resto es HTML inválido. - No puede contener espacios.
- Distingue mayúsculas de minúsculas:
#Sociosno encuentraid="socios". - Evita acentos y caracteres especiales aunque técnicamente HTML5 los permita, porque tendrás que codificarlos en la URL y se vuelve ilegible.
<!-- Problemático: hay que escribirlo codificado en el href -->
<h2 id="informacion-histórica">Información histórica</h2>
<a href="#informaci%C3%B3n-hist%C3%B3rica">Ir</a>
<!-- Mejor -->
<h2 id="informacion-historica">Información histórica</h2>
<a href="#informacion-historica">Ir</a>
Cómo procesa el navegador un fragmento
sequenceDiagram
autonumber
participant U as Usuario
participant N as Navegador
participant DOC as Documento en memoria
participant S as Servidor
U->>N: Clic en un enlace cuyo href es numeral torneos
N->>N: Compara origen, host, ruta y consulta<br/>con la URL actual
alt Solo cambia el fragmento
Note over N,S: No se envia ninguna peticion HTTP
N->>DOC: Busca un elemento con id igual a torneos
alt Lo encuentra
DOC-->>N: Nodo encontrado
N->>N: Lo marca como destino del fragmento<br/>y aplica la pseudoclase :target
N->>N: Hace scroll hasta ese nodo
N->>N: Mueve el foco del teclado al nodo
N->>N: Actualiza la barra de direcciones<br/>y agrega una entrada al historial
else No lo encuentra
DOC-->>N: Sin coincidencias
N->>N: No hace scroll, solo cambia la URL
end
else Cambia tambien la ruta
N->>S: GET /reglamento.html
S-->>N: 200 OK con el documento
Note over N: El fragmento se aplica<br/>despues de cargar
N->>N: Hace scroll al id indicado
end
Fragmentos hacia otro documento
Puedes combinar ruta y fragmento en el mismo href:
<a href="reglamento.html#cuotas">Ver la sección de cuotas del reglamento</a>
<a href="/torneos/2026-verano.html#inscripcion">Cómo inscribirse</a>
<a href="https://ninjas.cl/books/superclubnet/#_enlaces">Sección de enlaces del libro</a>
El navegador primero descarga el documento y luego, cuando termina de construirlo, salta al fragmento. Si el elemento con ese id se inserta más tarde con JavaScript, el salto ya ocurrió y no pasará nada.
Mejorando la experiencia con CSS
Dos propiedades hacen que la navegación por fragmentos se sienta bien:
/* Desplazamiento suave en vez de un salto brusco */
html { scroll-behavior: smooth; }
/* Respeta a quien pidió menos movimiento en su sistema operativo */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}
:target {
/* Deja espacio arriba para que un encabezado fijo no tape el destino */
scroll-margin-top: 5rem;
/* Resalta la sección a la que se llegó */
background-color: #fff3cd;
transition: background-color 1.2s ease;
}
scroll-margin-top resuelve un problema clásico: si tienes una barra de navegación con position: fixed de 80 píxeles de alto, el scroll deja el título justo debajo de la barra y queda oculto. Esa propiedad le dice al navegador que deje ese margen al hacer scroll.
La pseudoclase :target selecciona el elemento cuyo id coincide con el fragmento actual de la URL. Es CSS puro reaccionando al estado de la URL, sin JavaScript.
Fragmentos de texto
Los navegadores basados en Chromium y algunos otros soportan una extensión llamada text fragments, que permite enlazar a un texto concreto aunque no tenga id. La sintaxis usa el prefijo :~:text= dentro del fragmento:
https://ejemplo.cl/reglamento.html#:~:text=cuota%20mensual
Al abrirlo, el navegador busca “cuota mensual” en la página, hace scroll y lo resalta. Es la función detrás de “Copiar enlace al fragmento resaltado” del menú contextual de Chrome. No está soportado en todos los navegadores, así que sirve para compartir enlaces, no para construir la navegación de tu sitio.
Enlaces con protocolo: mailto, tel y compañía
Hasta aquí todos los href apuntaban a documentos. Pero el esquema de una URI puede indicarle al navegador que no debe navegar, sino delegar la acción al sistema operativo.
flowchart TD
CLICK["El usuario hace clic en un enlace"] --> SCHEME{"Que esquema tiene el href?"}
SCHEME -->|"http o https"| WEB["El navegador hace<br/>una peticion HTTP<br/>y renderiza el documento"]
SCHEME -->|"solo un fragmento"| HASH["El navegador hace scroll<br/>dentro del documento actual"]
SCHEME -->|"mailto:"| MAIL["El navegador entrega la URI<br/>al sistema operativo"]
SCHEME -->|"tel:"| TEL["El navegador entrega la URI<br/>al sistema operativo"]
SCHEME -->|"sms:"| SMS["El navegador entrega la URI<br/>al sistema operativo"]
MAIL --> OS["El sistema busca la aplicacion<br/>registrada para ese esquema"]
TEL --> OS
SMS --> OS
OS --> OK{"Hay una aplicacion<br/>registrada?"}
OK -->|Si| APP["Se abre el cliente de correo,<br/>el marcador telefonico<br/>o la app de mensajes"]
OK -->|No| NADA["No ocurre nada visible<br/>o aparece un dialogo de eleccion"]
style CLICK fill:#1c5d99,color:#ffffff
style APP fill:#2a9d8f,color:#ffffff
style NADA fill:#e76f51,color:#ffffff
mailto
Abre el cliente de correo del usuario con un mensaje nuevo ya preparado.
<!-- Forma mínima -->
<a href="mailto:[email protected]">Escríbenos</a>
<!-- Con asunto -->
<a href="mailto:[email protected]?subject=Consulta%20sobre%20inscripciones">
Consultar por inscripciones
</a>
<!-- Con asunto, copia y cuerpo -->
<a href="mailto:[email protected]?subject=Inscripci%C3%B3n%20al%20torneo&[email protected]&body=Hola%2C%20quiero%20inscribirme%20en%20el%20torneo%20de%20verano.">
Inscribirme por correo
</a>
<!-- Varios destinatarios -->
<a href="mailto:[email protected],[email protected]">Escribir a la directiva</a>
Los parámetros que puedes usar después del ?:
| Parámetro | Qué hace |
|---|---|
subject | Rellena el asunto |
body | Rellena el cuerpo del mensaje |
cc | Copia visible |
bcc | Copia oculta |
Todos deben ir codificados en porcentaje: los espacios como %20, los acentos en su forma UTF-8 codificada, los saltos de línea como %0A. Si escribes un espacio literal o un & sin codificar, el enlace se corta en ese punto.
Un detalle importante: mailto: depende por completo de que el usuario tenga un cliente de correo configurado. En un computador con Gmail en el navegador y sin cliente instalado, puede que no pase nada. Por eso, en una página de contacto, muestra siempre la dirección en texto además del enlace:
<p>Escríbenos a <a href="mailto:[email protected]">[email protected]</a></p>
Así, si el enlace no funciona, el usuario todavía puede copiar la dirección.
tel
Abre el marcador telefónico. En un teléfono, deja el número listo para llamar; en un escritorio, puede abrir Skype, Teams o una aplicación de VoIP.
<a href="tel:+56912345678">+56 9 1234 5678</a>
La regla de oro: el href debe usar formato internacional E.164 (signo +, código de país, número sin espacios ni guiones ni paréntesis), mientras que el texto visible puede tener el formato legible que quieras. Así el enlace funciona sin importar desde qué país lo abran.
<!-- Correcto: href limpio, texto legible -->
<a href="tel:+56221234567">(2) 2123 4567</a>
<!-- Incorrecto: caracteres de formato dentro del href -->
<a href="tel:(2) 2123-4567">(2) 2123-4567</a>
También se admiten extensiones con , (pausa) o con el parámetro ext:
<a href="tel:+56221234567,,102">Recepción, anexo 102</a>
Otros esquemas útiles
| Esquema | Ejemplo | Qué hace |
|---|---|---|
mailto: | mailto:[email protected] | Redacta un correo |
tel: | tel:+56912345678 | Inicia una llamada |
sms: | sms:+56912345678 | Abre un mensaje de texto |
geo: | geo:-33.4489,-70.6693 | Abre la app de mapas en esas coordenadas |
ftp: | ftp://ftp.ejemplo.cl/pub/ | Abre un servidor FTP |
file: | file:///home/usuario/doc.html | Abre un archivo local |
data: | data:text/plain,Hola | Contiene el recurso dentro de la propia URI |
javascript: | javascript:alert(1) | Ejecuta código; no lo uses, es un antipatrón |
Sobre javascript: en un href: rompe la navegación, es bloqueado por políticas de seguridad de contenido y confunde a las tecnologías asistivas. Si necesitas ejecutar código, usa <button> y un manejador de eventos.
Codificación porcentual
Las URIs solo admiten un conjunto limitado de caracteres. Todo lo demás debe representarse como % seguido del valor hexadecimal de cada byte en UTF-8.
| Carácter | Codificado | Por qué hay que codificarlo |
|---|---|---|
| espacio | %20 | Separa tokens en HTTP |
? | %3F | Inicia la consulta |
# | %23 | Inicia el fragmento |
& | %26 | Separa parámetros |
= | %3D | Separa clave de valor |
+ | %2B | En una consulta significa espacio |
/ | %2F | Separa segmentos de ruta |
á | %C3%A1 | Fuera de ASCII, dos bytes en UTF-8 |
ñ | %C3%B1 | Fuera de ASCII, dos bytes en UTF-8 |
| salto de línea | %0A | Carácter de control |
Puedes generar la codificación con JavaScript. La API real del lenguaje es encodeURIComponent() para trozos individuales y encodeURI() para una URI completa:
const destinatario = "[email protected]";
const asunto = "Inscripción al torneo de verano";
const cuerpo = "Hola,\nQuiero inscribirme en el torneo.\nGracias.";
const href =
"mailto:" + destinatario +
"?subject=" + encodeURIComponent(asunto) +
"&body=" + encodeURIComponent(cuerpo);
console.log(href);
// mailto:[email protected]?subject=Inscripci%C3%B3n%20al%20torneo%20de%20verano
// &body=Hola%2C%0AQuiero%20inscribirme%20en%20el%20torneo.%0AGracias.
La diferencia entre las dos funciones importa:
encodeURIComponent("a/b?c")devuelve"a%2Fb%3Fc": codifica todo lo que sea reservado, porque el resultado va a ser el valor de un parámetro.encodeURI("https://ejemplo.cl/a b")devuelve"https://ejemplo.cl/a%20b": respeta:,/,?y#porque asume que son parte de la estructura de la URI.
Regla: usa encodeURIComponent para cada valor que insertes en una consulta, y encodeURI solo cuando tengas una URI casi completa a la que le falta escapar espacios.
El atributo target
target indica dónde debe abrirse el destino.
| Valor | Comportamiento |
|---|---|
_self | En el mismo contexto de navegación. Es el valor por defecto |
_blank | En una pestaña o ventana nueva |
_parent | En el contexto padre; relevante si la página está dentro de un <iframe> |
_top | En el contexto más alto de la jerarquía, rompiendo todos los <iframe> |
| Un nombre | En el <iframe> cuyo atributo name coincida |
<a href="https://ninjas.cl/" target="_blank" rel="noopener noreferrer">
Ninjas.cl (se abre en una pestaña nueva)
</a>
Sobre _blank hay que ser honesto: es una de las decisiones más discutidas del desarrollo web.
Argumentos en contra. Le quita al usuario el control sobre su propia navegación. Rompe el botón “Atrás”, porque en la pestaña nueva no hay historial al cual volver. En móviles, donde la gestión de pestañas es incómoda, acumula pestañas que el usuario no pidió. Y para quien usa un lector de pantalla, el cambio de contexto puede ocurrir sin aviso.
Cuándo se justifica. Cuando el usuario está en medio de un proceso que perdería al salir: un formulario a medio llenar, un reproductor de video, un carrito de compras. Ahí abrir los términos y condiciones en pestaña nueva evita destruir su trabajo.
Si lo usas, avísalo:
<a href="/terminos.pdf" target="_blank" rel="noopener">
Términos y condiciones
<span aria-hidden="true">↗</span>
<span class="solo-lectores">(se abre en una pestaña nueva)</span>
</a>
La clase .solo-lectores es el patrón habitual para texto que solo leen las tecnologías asistivas: se saca de la vista sin usar display: none, que lo ocultaría también a los lectores de pantalla.
.solo-lectores {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px; border: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
El atributo rel
rel describe la relación entre el documento actual y el destino. Los valores más útiles:
| Valor | Para qué sirve |
|---|---|
noopener | Impide que la página abierta acceda a window.opener y pueda manipular la pestaña original |
noreferrer | Impide que se envíe la cabecera Referer; implica noopener |
nofollow | Indica a los buscadores que no transfieran reputación a ese enlace |
ugc | Marca contenido generado por usuarios, como comentarios |
sponsored | Marca enlaces pagados o de afiliación |
external | Señala que el destino está fuera del sitio actual |
me | Indica que el destino es otro perfil de la misma persona; se usa en la verificación de Mastodon |
El caso de noopener merece explicación. Sin él, la página que se abre con target="_blank" recibe una referencia a la ventana original en window.opener y puede cambiar su ubicación. Un sitio malicioso podía reemplazar tu pestaña original por una página falsa de inicio de sesión mientras el usuario miraba la pestaña nueva. Esta técnica se llama tabnabbing.
Los navegadores modernos aplican noopener de forma implícita cuando hay target="_blank", así que hoy es una red de seguridad más que una necesidad. Escribirlo igual no cuesta nada y protege a usuarios con navegadores antiguos.
<a href="https://sitio-externo.cl" target="_blank" rel="noopener noreferrer">Sitio externo</a>
El atributo download
Convierte un enlace en una descarga en lugar de una navegación.
<!-- Descarga con el nombre original del archivo -->
<a href="/documentos/reglamento.pdf" download>Descargar el reglamento</a>
<!-- Descarga renombrando el archivo -->
<a href="/documentos/r-2026-v3-final.pdf" download="reglamento-super-club-net.pdf">
Descargar el reglamento
</a>
Dos restricciones que hay que conocer:
downloadsolo funciona si el recurso está en el mismo origen que la página (mismo esquema, host y puerto). Para recursos de otro origen el navegador lo ignora y navega normalmente. La excepción son las URIsblob:ydata:.- El servidor puede sobrescribir el nombre mediante la cabecera
Content-Disposition.
Otros atributos del enlace
<a href="/manual.pdf"
type="application/pdf"
hreflang="es"
title="Manual del socio, PDF de 2 MB"
referrerpolicy="no-referrer">
Manual del socio
</a>
| Atributo | Función |
|---|---|
type | Sugiere el tipo MIME del destino; es una pista, no una garantía |
hreflang | Indica el idioma del documento de destino |
title | Muestra un tooltip al pasar el mouse; no lo uses como única fuente de información, porque no aparece en móviles ni al navegar con teclado |
referrerpolicy | Controla cuánta información de origen se envía en la cabecera Referer |
ping | Lista de URLs a las que el navegador enviará una petición POST al hacer clic; se usa para analítica y muchos navegadores lo ignoran o lo bloquean |
Accesibilidad de los enlaces
Un enlace bien hecho es un enlace que cualquiera puede usar.
El texto debe tener sentido fuera de contexto. Los lectores de pantalla ofrecen una lista de todos los enlaces de la página. Una lista con quince entradas que dicen “clic aquí” es inservible.
<!-- Malo -->
<p>Para ver el reglamento <a href="/reglamento.pdf">haz clic aquí</a>.</p>
<!-- Bueno -->
<p>Revisa el <a href="/reglamento.pdf">reglamento del club en PDF</a>.</p>
Nunca dejes un enlace sin texto accesible. Un enlace que solo contiene un ícono necesita una etiqueta:
<!-- Malo: no hay nada que anunciar -->
<a href="/carrito"><svg><!-- ícono --></svg></a>
<!-- Bueno -->
<a href="/carrito" aria-label="Ver el carrito de compras">
<svg aria-hidden="true" focusable="false"><!-- ícono --></svg>
</a>
El foco debe verse. Nunca escribas outline: none sin poner otro indicador en su lugar. Quien navega con teclado depende de ese anillo para saber dónde está.
/* Nunca hagas esto solo */
a:focus { outline: none; }
/* Haz esto */
a:focus-visible { outline: 3px solid #ffb703; outline-offset: 2px; }
El área de clic debe ser suficientemente grande. Las guías de accesibilidad recomiendan un objetivo táctil de al menos 24 por 24 píxeles CSS. En navegación y botones, agrega padding en vez de depender del tamaño de la fuente.
Marca los enlaces del sitio actual. Si tu barra de navegación resalta la página actual, indícalo también semánticamente:
<nav aria-label="Principal">
<ul>
<li><a href="/">Inicio</a></li>
<li><a href="/torneos/" aria-current="page">Torneos</a></li>
<li><a href="/contacto/">Contacto</a></li>
</ul>
</nav>
Ofrece un enlace para saltar al contenido. Es el primer elemento enfocable de la página y permite a quien usa teclado saltarse la navegación:
<body>
<a class="saltar" href="#contenido">Saltar al contenido principal</a>
<nav><!-- muchos enlaces --></nav>
<main id="contenido" tabindex="-1"><h1>Super Club Net</h1></main>
</body>
.saltar { position: absolute; left: -9999px; top: 0; background: #1c5d99; color: #ffffff; padding: 0.75rem 1rem; z-index: 100; }
.saltar:focus { left: 0; }
Proyecto integrador: la navegación del Super Club Net
Juntemos todo en una página completa que usa cada tipo de enlace visto en el capítulo.
index.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Super Club Net</title>
<link rel="stylesheet" href="css/estilos.css">
</head>
<body>
<a class="saltar" href="#contenido">Saltar al contenido principal</a>
<header>
<h1><a href="/" class="marca">Super Club Net</a></h1>
<nav aria-label="Principal">
<ul class="menu">
<li><a href="/" aria-current="page">Inicio</a></li>
<li><a href="/torneos/">Torneos</a></li>
<li><a href="/socios/">Socios</a></li>
<li><a href="#contacto">Contacto</a></li>
</ul>
</nav>
</header>
<main id="contenido" tabindex="-1">
<section id="bienvenida">
<h2>Bienvenido</h2>
<p>
Somos un club deportivo fundado en 1998. Revisa nuestro
<a href="documentos/reglamento.pdf" download="reglamento-super-club-net.pdf">reglamento en PDF</a>
o mira el <a href="/torneos/2026-verano.html#inscripcion">proceso de inscripción del torneo</a>.
</p>
</section>
<section id="enlaces-utiles">
<h2>Enlaces útiles</h2>
<ul>
<li>
<a href="https://developer.mozilla.org/es/docs/Web/HTML" target="_blank" rel="noopener noreferrer">
Referencia de HTML en MDN <span aria-hidden="true">↗</span>
<span class="solo-lectores">(se abre en una pestaña nueva)</span>
</a>
</li>
<li>
<a href="https://ninjas.cl/books/superclubnet/" target="_blank" rel="noopener noreferrer">
Guía original del Super Club Net <span aria-hidden="true">↗</span>
<span class="solo-lectores">(se abre en una pestaña nueva)</span>
</a>
</li>
</ul>
</section>
<section id="contacto">
<h2>Contacto</h2>
<ul class="contacto">
<li>Correo: <a href="mailto:[email protected]?subject=Consulta%20desde%20el%20sitio">[email protected]</a></li>
<li>Teléfono: <a href="tel:+56221234567">(2) 2123 4567</a></li>
<li>Mensajes: <a href="sms:+56912345678">+56 9 1234 5678</a></li>
</ul>
<p><a href="#contenido">Volver arriba</a></p>
</section>
</main>
<footer>
<p>Super Club Net 2026 · <a href="/legal/privacidad.html">Política de privacidad</a></p>
</footer>
</body>
</html>
css/estilos.css:
:root {
--azul: #1c5d99;
--morado: #6b3fa0;
--rojo: #d62828;
--ambar: #ffb703;
--texto: #1f2933;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; line-height: 1.7; color: var(--texto); }
/* ---- Enlace de salto ---- */
.saltar {
position: absolute;
left: -9999px; top: 0; z-index: 100;
background: var(--azul); color: #ffffff;
padding: 0.75rem 1rem;
text-decoration: none;
}
.saltar:focus { left: 0; }
/* ---- Estados de enlace en orden LVHA ---- */
a:link { color: var(--azul); text-underline-offset: 0.15em; }
a:visited { color: var(--morado); }
a:focus-visible { outline: 3px solid var(--ambar); outline-offset: 3px; border-radius: 3px; }
a:hover { color: var(--rojo); text-decoration-thickness: 2px; }
a:active { color: #9d0208; }
/* ---- Cabecera y navegación ---- */
header { border-bottom: 2px solid #e4e7eb; padding: 1rem 1.5rem; }
.marca { text-decoration: none; color: var(--texto); }
.menu { list-style: none; display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0; margin: 0.5rem 0 0; }
.menu a { display: inline-block; padding: 0.6rem 0.9rem; text-decoration: none; border-radius: 4px; }
.menu a:hover { background: #f0f4f8; }
.menu a[aria-current="page"] { background: var(--azul); color: #ffffff; }
/* ---- Contenido ---- */
main { max-width: 48rem; margin: 0 auto; padding: 2rem 1.5rem; }
main:focus { outline: none; }
section { margin-bottom: 3rem; scroll-margin-top: 2rem; }
.contacto { list-style: none; padding: 0; }
.contacto li { padding: 0.35rem 0; }
footer { border-top: 2px solid #e4e7eb; padding: 1.5rem; text-align: center; font-size: 0.9rem; }
/* ---- Utilidades ---- */
.solo-lectores {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px; border: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
/* ---- Desplazamiento suave, respetando preferencias ---- */
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
Leer una URL desde JavaScript
Aunque el DOM lo veremos en detalle más adelante, vale la pena mostrar la API que el navegador ofrece para trabajar con URLs, porque es la mejor forma de comprobar que entendiste sus componentes.
Abre la consola del navegador y pega esto:
const url = new URL(
"https://blog.ejemplo.cl:8443/articulos/2026/enlaces?orden=fecha&pagina=2#comentarios"
);
console.log(url.protocol); // "https:"
console.log(url.host); // "blog.ejemplo.cl:8443"
console.log(url.hostname); // "blog.ejemplo.cl"
console.log(url.port); // "8443"
console.log(url.pathname); // "/articulos/2026/enlaces"
console.log(url.search); // "?orden=fecha&pagina=2"
console.log(url.hash); // "#comentarios"
console.log(url.origin); // "https://blog.ejemplo.cl:8443"
url.searchParams.forEach(function (valor, clave) {
console.log(clave + " = " + valor);
});
// orden = fecha
// pagina = 2
Un detalle a notar: url.protocol incluye los dos puntos ("https:") y url.hash incluye el numeral ("#comentarios"). Es un tropiezo clásico al comparar valores.
El constructor URL también resuelve rutas relativas, lo que te permite verificar tus propias resoluciones sin subir nada a un servidor:
new URL("../otro.html", "https://ejemplo.cl/a/b/c.html").href;
// "https://ejemplo.cl/a/otro.html"
new URL("/raiz.html", "https://ejemplo.cl/a/b/c.html").href;
// "https://ejemplo.cl/raiz.html"
new URL("#seccion", "https://ejemplo.cl/a/b/c.html").href;
// "https://ejemplo.cl/a/b/c.html#seccion"
Errores comunes
| Error | Causa | Solución |
|---|---|---|
| El enlace no cambia de estilo al pasar el mouse en páginas ya visitadas | a:hover se declaró antes de a:visited y pierde por orden de cascada | Escribe las pseudoclases en orden LVHA: :link, :visited, :focus, :hover, :active |
| El enlace no se ve como enlace ni cambia el cursor | Se escribió <a> sin atributo href | Un <a> sin href no es un enlace; agrégale href o usa <button> |
El #seccion no lleva a ninguna parte | No existe ningún elemento con ese id, o hay diferencia de mayúsculas y minúsculas | Verifica que el id exista y coincida exactamente; los id distinguen mayúsculas |
El destino del # queda tapado por la barra de navegación fija | El scroll deja el elemento en el borde superior, debajo del encabezado con position: fixed | Aplica scroll-margin-top con la altura de la barra al elemento destino |
| La página funciona local pero da 404 al publicar | Diferencias de mayúsculas en nombres de archivo entre tu sistema y el servidor Linux | Usa siempre minúsculas y guiones en nombres de archivo y carpetas |
/estilos.css no carga cuando el sitio está en una subcarpeta | La barra inicial apunta a la raíz del dominio, no a la raíz del proyecto | Usa rutas relativas o define correctamente la etiqueta <base> |
El mailto corta el mensaje a la mitad | El body contenía un &, un # o un espacio sin codificar | Codifica cada valor con encodeURIComponent() antes de armar el href |
El enlace tel: no llama en algunos países | El número está en formato local o tiene guiones y paréntesis dentro del href | Usa formato E.164 en el href (tel:+56912345678) y deja el formato legible en el texto visible |
| Se abren pestañas nuevas que el usuario no pidió | Se aplicó target="_blank" a todos los enlaces por costumbre | Reserva _blank para casos donde el usuario perdería trabajo, y anúncialo en el texto |
| Al hacer clic en un enlace externo el sitio original es reemplazado por una página falsa | Tabnabbing: la página abierta manipuló window.opener | Agrega rel="noopener noreferrer" a los enlaces con target="_blank" |
download no descarga y navega a la página | El recurso está en otro origen distinto al de la página | download solo funciona en el mismo origen; sirve el archivo desde tu propio dominio |
| El lector de pantalla anuncia “enlace” pero no dice a dónde va | El enlace solo contiene un ícono o una imagen sin alt | Agrega alt descriptivo a la imagen o aria-label al enlace |
| Un enlace relativo sube un nivel de más | La URL actual no termina en barra, así que el último segmento se trata como archivo | Termina las URLs de directorio con / o usa rutas absolutas de raíz |
| El navegador no muestra el foco al navegar con Tab | Alguien escribió outline: none para quitar el borde azul | Usa :focus-visible con un outline propio, visible y con buen contraste |
Un href="#" con JavaScript salta al inicio de la página | El comportamiento por defecto del fragmento vacío es ir al tope del documento | Usa <button type="button"> en vez de un enlace falso |
Ejercicios propuestos
-
Mapa del sitio. Crea una estructura de carpetas con
index.htmlen la raíz y en tres subcarpetas (torneos/,socios/,contacto/). Cadaindex.htmldebe tener una navegación que enlace a las otras tres usando solo rutas relativas. Verifica que todos los enlaces funcionen desde cualquier página. -
La misma navegación, dos veces. Duplica el ejercicio anterior pero usando rutas absolutas de raíz (
/torneos/). Sirve el sitio desde la raíz de un servidor local y luego desde una subcarpeta. Documenta qué se rompe en cada caso y por qué. -
Descomposición manual. Toma estas cinco URLs e identifica cada componente en una tabla (esquema, host, puerto, ruta, consulta, fragmento). Luego verifica tus respuestas con
new URL()en la consola del navegador.https://www.ejemplo.cl/tienda/producto?id=42&color=rojo#opinioneshttp://localhost:8080/api/v1/sociosftp://archivos.ejemplo.org:2121/publico/manual.pdfmailto:[email protected]?subject=Ayudahttps://ejemplo.cl:443/
-
Reglamento navegable. Escribe un documento con seis secciones. Al inicio, un índice con enlaces a cada una; al final de cada sección, un enlace de vuelta al índice. Agrega
scroll-behavior: smooth,scroll-margin-topy un estilo:targetque resalte la sección activa. -
Barra fija. Sobre el ejercicio anterior, agrega un encabezado con
position: fixedy altura de 4 rem. Comprueba que los títulos quedan tapados al saltar y corrígelo conscroll-margin-top. Mide la altura real con las herramientas de desarrollo. -
Generador de mailto. Construye una página con tres campos de texto (destinatario, asunto, mensaje) y un enlace que se actualice cada vez que escribes. Usa
encodeURIComponent()en cada valor. Prueba escribiendo texto con acentos, con&y con saltos de línea. -
Página de contacto completa. Crea una página que ofrezca correo (
mailto:con asunto prellenado), teléfono (tel:en formato E.164), mensaje (sms:) y ubicación (geo:con las coordenadas de tu ciudad). Cada dato debe estar también visible como texto plano. -
Auditoría de accesibilidad. Toma cualquier página que hayas hecho en capítulos anteriores y revisa todos sus enlaces contra esta lista: texto con sentido fuera de contexto, foco visible, área de clic de al menos 24 píxeles,
aria-currenten la página actual, enlace de salto al contenido, aviso en los que abren pestaña nueva. Corrige lo que falte. -
Comparación de resolución. En la consola del navegador, ejecuta
new URL(relativa, base).hrefpara al menos ocho combinaciones distintas de rutas relativas y URLs base, incluyendo bases con y sin barra final. Anota los resultados y explica cada uno. -
Detector de enlaces externos. Escribe un script que recorra todos los
<a>del documento, detecte cuáles apuntan a un dominio distinto del actual comparandourl.origin, y les agregue automáticamenterel="noopener noreferrer"más un ícono indicador. Es un buen ejercicio para practicar la APIURLantes de entrar al DOM.
Lo que viene
Con este capítulo la navegación de tu sitio ya está resuelta: sabes conectar documentos entre sí, saltar dentro de uno solo, apuntar a recursos externos y sacar al usuario del navegador cuando hace falta. También tienes el vocabulario correcto para hablar de direcciones web sin ambigüedades, algo que te va a servir cuando trabajes con formularios, con peticiones desde JavaScript y con enrutamiento del lado del servidor.
En el capítulo 7 llevaremos el enlace un paso más allá: veremos los mapas de imagen, que permiten definir zonas clickeables con distintas formas dentro de una misma imagen; la etiqueta <iframe>, que incrusta un documento completo dentro de otro y explica por fin para qué existen los valores _parent y _top del atributo target; y los divisores de contenido, esos elementos que estructuran visualmente una página sin agregar significado semántico. Si quieres repasar la ruta completa del curso, el índice está en /tecnologias/superclubnet/00-indice/.