🎯 Objetivo del tema
El administrador aún no ha insertado un video para esta sección.
🗺️ Mapa del tema
En el Tema 1.4 aprendiste accesibilidad web básica. Ahora vas al siguiente nivel: ARIA, navegación con teclado, manejadores de foco en SPAs (que es más complejo que en HTML estático), y SEO técnico para SPAs (que es un reto porque Googlebot no ejecuta JS tan bien como un navegador). Vamos a aplicar Lighthouse como auditor, y dejar tu app con 95+ en accesibilidad, performance, y SEO. Es lo que separa una app profesional de una que solo 'funciona'.
Por qué las SPAs necesitan manejo explícito del foco y regiones ARIA live.
Skip links, focus traps en modales, atajos, y orden lógico.
Cómo hacer que Google entienda tu SPA de React.
Cómo se ve tu link cuando lo compartes en Twitter, LinkedIn, WhatsApp.
LCP, FID, CLS: cómo medirlos y optimizarlos.
Puntaje 95+ en Accessibility, Performance, SEO, Best Practices.
3.7.1 Accesibilidad en SPAs: el reto del foco y ARIA
En HTML estático, el navegador maneja el foco y la navegación por teclado automáticamente. En SPAs de React, tú CONTROLAS la navegación: cambias el contenido sin recargar, pero el foco se queda en el botón que disparó el cambio. Si no lo manejas, el usuario con lector de pantalla no sabe que el contenido cambió. Es el reto #1 de accesibilidad en SPAs. La solución: manejo explícito del foco + ARIA live regions.
El patrón: cuando el contenido cambia, mover el foco
import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
// Patrón: cuando navegas a una nueva página, mueve el foco al H1
function DetallePais() {
const navigate = useNavigate();
const tituloRef = useRef(null);
// Cuando el componente se monta, mueve el foco al H1 (después de un tick)
useEffect(() => {
// Pequeño delay para asegurar que el DOM esté listo
setTimeout(() => tituloRef.current?.focus(), 100);
}, []);
return (
<article>
<h1 ref={tituloRef} tabIndex={-1}>{pais.name.common}</h1>
{/* ... */}
</article>
);
}
// ARIA live regions: anuncia cambios dinámicos al lector de pantalla
function Buscador() {
const [resultados, setResultados] = useState([]);
const [mensaje, setMensaje] = useState('');
useEffect(() => {
if (resultados.length === 0) {
setMensaje('No se encontraron resultados');
} else {
setMensaje(`${resultados.length} resultados encontrados`);
}
}, [resultados]);
return (
<div>
<input type="search" aria-label="Buscar país" />
{/* aria-live='polite' anuncia el mensaje cuando cambia */}
<div role="status" aria-live="polite" className="sr-only">
{mensaje}
</div>
{resultados.map(r => <PaisCard key={r.id} pais={r} />)}
</div>
);
}
// sr-only: clase que esconde visualmente pero es accesible para lectores de pantalla
// .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }Manejo del foco y ARIA live regions en React
El administrador aún no ha insertado un video para esta sección.
3.7.2 Navegación con teclado: skip links y focus traps
Un usuario con discapacidad visual o motora navega con teclado (Tab, Enter, Escape, flechas). Tu app debe soportarlo completamente. Las dos claves: skip links para saltar el menú, y focus traps en modales para que el usuario no se 'escape' a otro lado. Son detalles que la mayoría olvida, pero que hacen la diferencia entre 'usable con teclado' y 'frustrante con teclado'.
Skip Link: saltar al contenido principal
// components/SkipLink.jsx
import { Link } from 'react-router-dom';
export function SkipLink() {
return (
<a href="#main-content" className="skip-link">
Saltar al contenido principal
</a>
);
}
// En tu layout principal
<body>
<SkipLink />
<Header />
<main id="main-content" tabIndex={-1}>
<Outlet /> {/* React Router inyecta la página aquí */}
</main>
<Footer />
</body>
/* CSS: el skip link es INVISIBLE hasta que recibe foco */
.skip-link {
position: absolute;
top: -100px; /* fuera de la pantalla */
left: 0;
background: black;
color: white;
padding: 12px 24px;
z-index: 1000;
}
.skip-link:focus {
top: 0; /* aparece cuando recibe foco con Tab */
}Skip link: el primer elemento focuseable, oculto hasta que se usa
Focus trap en modales: que Tab se quede dentro del modal
// hooks/useFocusTrap.js
import { useEffect, useRef } from 'react';
export function useFocusTrap(isOpen) {
const ref = useRef(null);
useEffect(() => {
if (!isOpen) return;
const modal = ref.current;
const focusable = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const first = focusable[0];
const last = focusable[focusable.length - 1];
// Guardar el elemento que tenía foco antes de abrir el modal
const previouslyFocused = document.activeElement;
// Mover el foco al primer elemento del modal
first?.focus();
function handleTab(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus(); // shift+tab desde el primero: salta al último
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus(); // tab desde el último: vuelve al primero
}
}
function handleEscape(e) {
if (e.key === 'Escape') onClose();
}
modal.addEventListener('keydown', handleTab);
document.addEventListener('keydown', handleEscape);
return () => {
modal.removeEventListener('keydown', handleTab);
document.removeEventListener('keydown', handleEscape);
previouslyFocused?.focus(); // restaurar el foco al cerrar
};
}, [isOpen]);
return ref;
}
// Uso en un modal:
function Modal({ isOpen, onClose, children }) {
const ref = useFocusTrap(isOpen);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div ref={ref} className="modal-content" role="dialog" aria-modal="true" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
}useFocusTrap: que Tab se quede dentro del modal
El administrador aún no ha insertado un video para esta sección.
3.7.3 SEO técnico para SPAs: meta tags, sitemap, robots
Google puede EJECUTAR JavaScript (con Googlebot), pero es LENTO y a veces no ejecuta todo. Para SEO profesional, una SPA debe tener: meta tags en cada página (con React Helmet), un sitemap.xml que liste todas las URLs, y un robots.txt que le diga a Google qué indexar. Es el SEO técnico mínimo para que tu app sea encontrable.
Meta tags dinámicos con react-helmet-async
// Instalar: npm install react-helmet-async
import { Helmet } from 'react-helmet-async';
function DetallePais() {
const { data: pais } = useFetch(...);
if (!pais) return null;
const p = pais[0];
return (
<>
<Helmet>
{/* SEO básico */}
<title>{p.name.common} - Atlas Mundial</title>
<meta name="description" content={`Información sobre ${p.name.common}: capital ${p.capital[0]}, población ${p.population.toLocaleString()}.`} />
<link rel="canonical" href={`https://atlas-mundial.com/pais/${p.cca3}`} />
{/* Open Graph para redes sociales */}
<meta property="og:title" content={`${p.name.common} - Atlas Mundial`} />
<meta property="og:description" content={`Capital: ${p.capital[0]}, Población: ${p.population.toLocaleString()}`} />
<meta property="og:image" content={p.flags.png} />
<meta property="og:url" content={`https://atlas-mundial.com/pais/${p.cca3}`} />
<meta property="og:type" content="article"} />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={p.name.common} />
<meta name="twitter:image" content={p.flags.png} />
</Helmet>
<article>
<h1>{p.name.common}</h1>
{/* ... */}
</article>
</>
);
}
// En main.jsx, envuelve tu app con HelmetProvider
import { HelmetProvider } from 'react-helmet-async';
ReactDOM.createRoot(document.getElementById('root')).render(
<HelmetProvider>
<BrowserRouter><App /></BrowserRouter>
</HelmetProvider>
);Meta tags dinámicos con react-helmet-async
El administrador aún no ha insertado un video para esta sección.
3.7.4 Open Graph y meta tags para redes sociales
Cuando compartes un link en WhatsApp, Twitter, LinkedIn o Facebook, ves una tarjeta con imagen, título y descripción. Eso lo controlas TÚ con los meta tags Open Graph. Sin ellos, WhatsApp muestra solo el URL pelao. Con ellos, tienes una 'mini-publicidad' de tu app cada vez que alguien comparte. Es una de las inversiones con mayor retorno: 10 minutos de configuración, años de mejor exposición.
Las 5 Open Graph tags que DEBES tener
- og:title: el título que se muestra en la tarjeta (60 caracteres máx).
- og:description: la descripción (155 caracteres máx).
- og:image: la imagen (1200x630px para Facebook/LinkedIn, 800x418 para Twitter).
- og:url: la URL canónica de la página.
- og:type: 'website' para home, 'article' para posts, 'product' para productos.
El administrador aún no ha insertado un video para esta sección.
3.7.5 Core Web Vitals: las 3 métricas de Google
Google usa 3 métricas para rankear tu sitio: Largest Contentful Paint (LCP, qué tan rápido carga el contenido principal), First Input Delay (FID, qué tan rápido responde a interacciones), y Cumulative Layout Shift (CLS, qué tanto 'salta' el contenido). Son el triángulo de la performance web moderna. Si las 3 están en verde, tu sitio rankea mejor.
| Métrica | Qué mide | Bueno | Cómo optimizar |
|---|---|---|---|
| LCP (Largest Contentful Paint) | Cuánto tarda en cargar el contenido principal visible. | < 2.5 segundos. | Optimiza imágenes, usa lazy loading, preconnect a CDNs. |
| FID (First Input Delay) | Cuánto tarda en responder al primer click/tecla. | < 100 ms. | Reduce JS bloqueante, usa code splitting, evita tareas pesadas en main thread. |
| CLS (Cumulative Layout Shift) | Cuánto 'salta' el contenido mientras carga. | < 0.1. | Define width/height en imágenes, no insertes contenido arriba, usa font-display: swap. |
El administrador aún no ha insertado un video para esta sección.
3.7.6 Lighthouse: auditoría completa
Lighthouse es la herramienta de auditoría de Google: mide accesibilidad, performance, SEO, best practices, y PWA en tu sitio. Se ejecuta desde Chrome DevTools > Lighthouse. El estándar profesional: 95+ en las 4 categorías. Si tu sitio no pasa, NO lo publiques. Es la línea base de 'production-ready' en el frontend moderno.
Cómo correr Lighthouse + interpretar resultados
// 1. Abrir Chrome DevTools (F12)
// 2. Pestaña 'Lighthouse'
// 3. Marcar: Accessibility, Best Practices, SEO, Performance
// 4. Seleccionar 'Mobile' o 'Desktop'
// 5. Click 'Analyze page load'
// 6. Espera 30-60 segundos
// 7. Revisa el puntaje (0-100) en cada categoría
// O desde la línea de comandos (más detallado):
// npm install -g lighthouse
// lighthouse https://tu-app.com --view
// El estándar profesional:
// Accessibility >= 95 (ideal 100)
// Best Practices >= 95
// SEO >= 95
// Performance >= 90 (depende de la app)
//
// Si alguna está en rojo (< 50), hay problemas GRAVES.
// Si está en naranja (50-89), hay mejoras importantes.
// Si está en verde (90-100), tu sitio está production-ready.Cómo correr Lighthouse y leer el reporte
El administrador aún no ha insertado un video para esta sección.
📚 Contenido ampliado
Material adicional, referencias externas verificadas y ejemplos extendidos.
🤖 AI Mission
Lighthouse es tu auditor automático. Si no pasa 95+, no publiques.
Misión: Toma tu Atlas Mundial y mejórala hasta pasar 95+ en Lighthouse en las 4 categorías. Instala react-helmet-async para SEO, agrega skip link y focus management, optimiza las Core Web Vitals, y agrega los meta tags de Open Graph. Pídele a la IA que audite tu sitio. Anota: ¿cuál métrica fue la más difícil de optimizar?
Pasos sugeridos
- Parte del proyecto atlas-mundial.
- Instala: npm install react-helmet-async. Envuelve tu app con HelmetProvider en main.jsx.
- Agrega Helmet a DetallePais con title, meta description, og:title, og:description, og:image, og:url.
- Agrega Helmet a Home con title genérico y meta description.
- Crea un componente SkipLink y agrégalo al inicio del MainLayout. Acepta el id en <main>.
- Implementa useFocusTrap en tu Modal (si tienes) y mueve el foco al H1 después de cada navegación.
- Agrega aria-live='polite' a tu buscador para anunciar resultados al lector de pantalla.
- Crea una imagen Open Graph: 1200x630px con el logo de Atlas Mundial.
- Ejecuta Lighthouse en Chrome DevTools. Mide: Accessibility, Best Practices, SEO, Performance.
- Si Lighthouse < 95 en alguna categoría, identifica y corrige los problemas.
- Optimiza Core Web Vitals: lazy loading en imágenes, preconnect a APIs externas, evita JS bloqueante.
- Pídele a la IA: 'Tengo Lighthouse en mi app. Sugiere 2 mejoras de accesibilidad y 2 de performance. NO me des código, dime QUÉ cambiar y por qué.'
- Aplica las 4 mejoras.
- Anota en tu cuaderno: puntaje inicial vs final, cuál métrica fue la más difícil, qué aprendiste sobre accesibilidad que no sabías.
📓 Entregable: URL pública, capturas de Lighthouse con 95+ en las 4 categorías, código de Helmet y SkipLink, y 1 página de cuaderno con la comparativa de puntajes.
🚫 Errores típicos de razonamiento
Por qué: Sin react-helmet-async, TODAS las páginas tienen el mismo <title>. Google ve 50 páginas con el mismo título y las rankea mal. Un usuario que busca 'información de Argentina' nunca encuentra tu página. Es el error #1 de SEO en SPAs. Solución: Helmet con title y meta description DINÁMICOS por ruta.
Por qué: Sin og:title, og:description, og:image, cuando compartes tu link en WhatsApp/Twitter/LinkedIn se ve solo el URL. Es un 'mini-anuncio' perdido. Con Open Graph bien configurado, ves una tarjeta con imagen, título, y descripción. 3x más clicks. Es 10 minutos de configuración para años de mejor exposición.
Por qué: Si tu página carga el header, luego un banner arriba, luego el contenido, hay un 'salto' visible (CLS alto). Google penaliza esto: tu sitio rankea peor. Solución: define width/height en imágenes y contenedores, usa font-display: swap, y reserva espacio para contenido que se carga tarde. La regla: el layout no debe 'saltar' mientras carga.
Por qué: Si abres un modal y el usuario hace Tab, el foco se 'escapa' al header detrás del modal. El usuario queda perdido. Solución: useFocusTrap que captura Tab y hace que se mantenga dentro del modal hasta cerrarlo. Es 15 líneas, mejora la experiencia del 100% de usuarios con teclado.
Por qué: Si solo pruebas con mouse, te perderás el 30% de usuarios que usan teclado (discapacidad motora, power users). Si no pruebas con lector de pantalla (NVDA, VoiceOver), te perderás el 15% con discapacidad visual. Es como probar una app móvil solo en iPhone: te pierdes el 70% de usuarios Android. Herramientas: Lighthouse Accessibility, axe DevTools, y un test manual con Tab + lector de pantalla. Es 30 minutos que te ahorran horas de soporte.
🧪 Laboratorio práctico
Una app sin accesibilidad es una app que excluye al 15% de la humanidad.
Laboratorio: Atlas Mundial con accesibilidad WCAG AA + SEO técnico + Lighthouse 95+
Objetivo: Llevar Atlas Mundial al nivel production-ready en accesibilidad y SEO: skip link, focus management, ARIA, meta tags dinámicos con Helmet, Open Graph, sitemap.xml, y robots.txt. El resultado: 95+ en Lighthouse en las 4 categorías, accesible con teclado, encontrable en Google.
Pasos
- Instala: npm install react-helmet-async.
- Envuelve App con HelmetProvider en main.jsx.
- Agrega Helmet a cada página con title, meta description, og:title, og:description, og:image, og:url, og:type.
- Crea public/og-image.png: 1200x630px con logo + texto descriptivo.
- Crea un componente SkipLink. Agrégalo al inicio del MainLayout. El <main> tiene id='main-content' y tabIndex={-1}.
- Implementa useFocusTrap en tu Modal. Mueve el foco al H1 después de cada navegación de React Router.
- Agrega aria-live='polite' a tu buscador. Agrega aria-label a inputs sin label visible. Agrega role='status' a mensajes de cargando.
- Crea public/sitemap.xml con todas tus rutas: <url><loc>https://tu-dominio.com/</loc></url>.
- Crea public/robots.txt: User-agent: * / Allow: / / Sitemap: https://tu-dominio.com/sitemap.xml.
- Ejecuta Lighthouse en Chrome DevTools. Mide las 4 categorías. Anota los puntajes.
- Si alguna < 95, identifica los problemas y corrige uno por uno (Lighthouse te dice exactamente qué).
- Prueba con teclado: Tab debe llevarte a través de toda la página, Enter debe activar, Escape debe cerrar modales.
- Sube a Vercel. Comparte URL.
- Haz commit: 'feat: accesibilidad WCAG AA + SEO técnico + Lighthouse 95+'.
📓 Entregable: URL pública con Lighthouse 95+, capturas de las 4 categorías, código de los meta tags, screenshot del sitemap.xml y robots.txt, y commit en Git.