Contents
Introducción
Este tutorial muestra paso a paso cómo implementar infinite scroll en un tema de WordPress usando la REST API y JavaScript moderno. Verás alternativas, código listo para pegar en functions.php, el script cliente que consume la API, estilos del loader, buenas prácticas de accesibilidad, SEO y manejo de errores. El enfoque usa la API REST nativa (wp/v2/posts) y IntersectionObserver para un scroll eficiente y sin bloqueos.
Concepto y cuándo usar infinite scroll
- Qué es: Cargar más entradas automáticamente conforme el usuario se acerca al final de la lista.
- Cuándo usar: Listados largos (blog, portfolio, tienda), cuando la intención es exploratoria. Evita en contenidos que necesiten referencias claras de paginación (SEO muy sensible, o navegaciones profundas donde los usuarios marcan páginas).
- Consideraciones: Mantener accesibilidad, proporcionar fallback (paginación clásica), evitar carga infinita sin fin y manejar correctamente el historial (pushState si es necesario).
Enfoques posibles
- Usar el endpoint nativo /wp/v2/posts y leer los headers para saber el número total de páginas.
- Crear un endpoint REST personalizado si necesitas estructura de respuesta distinta (campos ACF, metadatos, o filtros complejos).
- Hacer paginación clásica como fallback para usuarios sin JavaScript o para bots.
Preparación en WordPress (functions.php)
Lo mínimo que necesitas en el servidor es encolar el script y pasar datos útiles (URL de la REST API, nonce si vas a usar endpoints que requieren permisos, posts_por_page inicial, etc.). Si usas el endpoint nativo no es obligatorio crear un endpoint nuevo.
Ejemplo: encolar script y pasar datos
// functions.php function mytheme_enqueue_infinite_scroll() { wp_enqueue_script( mytheme-infinite-scroll, get_template_directory_uri() . /js/infinite-scroll.js, array(), 1.0, true ) // Datos que usará el script: REST URL, posts por página inicial y nonce para acciones protegidas wp_localize_script( mytheme-infinite-scroll, MyThemeInfiniteScroll, array( rest_url => esc_url_raw( rest_url( wp/v2/posts ) ), posts_per_page => intval( get_option( posts_per_page ) ?: 10 ), nonce => wp_create_nonce( wp_rest ) ) ) } add_action( wp_enqueue_scripts, mytheme_enqueue_infinite_scroll )
Nota sobre endpoints personalizados
Si necesitas exponer campos adicionales (ACF, campos privados o transformar la respuesta), registra una ruta REST personalizada. En el ejemplo siguiente devolvemos posts con _embed para facilitar acceso a la imagen destacada y leemos el total de páginas.
// functions.php (opcional: endpoint personalizado) add_action( rest_api_init, function() { register_rest_route( mytheme/v1, /posts, array( methods => GET, callback => mytheme_rest_get_posts, args => array( page => array( required => false, default => 1, sanitize_callback => absint, ), per_page => array( required => false, default => 10, sanitize_callback => absint, ), ), ) ) } ) function mytheme_rest_get_posts( request ) { page = request->get_param( page ) per_page = request->get_param( per_page ) query = new WP_Query( array( post_type => post, post_status => publish, paged => page, posts_per_page => per_page, ) ) posts = array() foreach ( query->posts as post ) { posts[] = array( id => post->ID, title => get_the_title( post ), link => get_permalink( post ), excerpt => wp_trim_words( post->post_content, 30 ), // Añade campos extra si lo necesitas ) } // Añadimos header con número total de páginas response = new WP_REST_Response( posts ) response->header( X-Total-Pages, (int) query->max_num_pages ) return response }
Estructura HTML mínima (plantilla de tema)
En tu plantilla (index.php, archive.php o un template part) añade un contenedor para las entradas y un sentinel que servirá para IntersectionObserver. A continuación un ejemplo de la estructura que se recomienda insertar en el template:
ltmain id=main-contentgt ltsection id=posts-containergt lt!-- Aquí van los artículos renderizados por PHP inicialmente --gt lt?php if ( have_posts() ) : while ( have_posts() ) : the_post() ?gt ltarticle class=post-itemgt lth2gtlta href=lt?php the_permalink() ?gtgtlt?php the_title() ?gtlt/agtlt/h2gt ltdiv class=excerptgtlt?php the_excerpt() ?gtlt/divgt lt/articlegt lt?php endwhile endif ?gt lt/sectiongt ltdiv id=infinite-scroll-sentinel aria-hidden=truegtlt/divgt ltdiv id=infinite-scroll-loader aria-live=polite style=display:nonegtCargando…lt/divgt lt/maingt
JavaScript: Lógica del infinite scroll (cliente)
Usaremos fetch para consumir la API REST y IntersectionObserver para detectar cuando cargar la siguiente página. Leemos el header X-WP-TotalPages (en el endpoint nativo se llama así: X-WP-TotalPages) para saber cuándo detenernos.
// js/infinite-scroll.js (function () { if ( ! ( IntersectionObserver in window ) ) { // Fallback: no IntersectionObserver (p. ej. browsers antiguos) – podrías cargar botón Cargar más return } const apiURL = window.MyThemeInfiniteScroll window.MyThemeInfiniteScroll.rest_url /wp-json/wp/v2/posts const perPage = window.MyThemeInfiniteScroll window.MyThemeInfiniteScroll.posts_per_page ? parseInt(window.MyThemeInfiniteScroll.posts_per_page, 10) : 10 let currentPage = 1 // Ya renderizaste la página 1 en PHP si no, ajusta. let totalPages = null let loading = false const container = document.getElementById(posts-container) const sentinel = document.getElementById(infinite-scroll-sentinel) const loader = document.getElementById(infinite-scroll-loader) function showLoader() { if (loader) loader.style.display = block } function hideLoader() { if (loader) loader.style.display = none } async function fetchPosts(page) { loading = true showLoader() const url = new URL(apiURL, window.location.origin) url.searchParams.set(page, page) url.searchParams.set(per_page, perPage) url.searchParams.set(_embed, true) // para obtener imágenes destacadas embebidas try { const response = await fetch(url.toString(), { headers: { Accept: application/json, X-WP-Nonce: window.MyThemeInfiniteScroll window.MyThemeInfiniteScroll.nonce ? window.MyThemeInfiniteScroll.nonce : } }) if (!response.ok) { // Si la respuesta es 400 o 404 al pedir página más allá del límite, lo interpretamos como final if (response.status === 400 response.status === 404) { totalPages = currentPage // fuerza detener observer.disconnect() return [] } throw new Error(HTTP error response.status) } // Leer número total de páginas desde headers (endpoint nativo usa X-WP-TotalPages) const headerTotalPages = response.headers.get(X-WP-TotalPages) response.headers.get(X-Total-Pages) if (headerTotalPages) { totalPages = parseInt(headerTotalPages, 10) } const data = await response.json() return data } catch (err) { console.error(Error fetching posts:, err) return [] } finally { loading = false hideLoader() } } function renderPostItem(post) { // Si usas _embed, la imagen destacada puede estar en post._embedded[wp:featuredmedia][0].source_url const article = document.createElement(article) article.className = post-item article.innerHTML ={post.title.rendered post.title}
{post.excerpt post.excerpt.rendered ? post.excerpt.rendered : (post.excerpt )}return article } async function loadNextPage() { if (loading) return if (totalPages currentPage >= totalPages) { observer.disconnect() return } currentPage const posts = await fetchPosts(currentPage) if (!posts posts.length === 0) { // No hay más posts: desconecta el observer if (observer) observer.disconnect() return } posts.forEach(post => { const el = renderPostItem(post) container.appendChild(el) }) // Opcional: actualizar el historial (pushState) para reflejar la página visible // const url = new URL(window.location) // url.searchParams.set(page, currentPage) // history.replaceState({}, , url) } const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting !loading) { loadNextPage() } }) }, { root: null, rootMargin: 200px, threshold: 0 }) // Empezamos observando el sentinel observer.observe(sentinel) })()
Estilos CSS para loader y posts (opcional)
/ styles.css / #posts-container { display: grid grid-template-columns: 1fr gap: 1.25rem } .post-item { padding: 1rem border: 1px solid #e6e6e6 border-radius: 4px background: #fff } #infinite-scroll-loader { text-align: center padding: 1rem color: #444 }
Manejo de casos especiales y buenas prácticas
- Fallback para SEO y usuarios sin JS: Renderiza la primera página con PHP y deja la paginación clásica accesible (enlaces rel=next/prev). Los bots y rastreadores seguirán los enlaces.
- Evitar loops infinitos: Usa el header de total de páginas o un contador y desconecta el observer cuando no haya más contenido.
- Throttle y prevención de múltiples llamadas: Usa una variable loading y evita disparos concurrentes. IntersectionObserver con rootMargin reduce llamadas innecesarias.
- Accesibilidad: Indica al usuario con aria-live cuando se cargan nuevos elementos, y ofrece un botón Cargar más visible si el usuario lo prefiere.
- Rendimiento: Carga fragmentos más pequeños en móviles si es necesario y habilita caching en el servidor/CDN.
- Imagen destacada: Solicita _embed para evitar peticiones adicionales, pero recuerda que el payload puede crecer.
- Historial y URLs: Si quieres que cada scroll sea navegable, actualiza el historial con history.pushState/replaceState y gestiona el estado para restaurar la posición al volver atrás.
Checklist para implementación
- Encolar y localizar el script con rest_url, posts_per_page y nonce según corresponda.
- Renderizar inicialmente la página 1 con PHP para soporte SEO y sin-JS.
- Agregar sentinel y loader en la plantilla.
- Usar IntersectionObserver y fetch con manejo de headers para leer X-WP-TotalPages.
- Desconectar observer al alcanzar la última página y mostrar mensaje final si procede.
- Probar en navegadores móviles y desktop, y revisar rendimiento y accesibilidad.
Problemas frecuentes y soluciones
- No carga más páginas: Verifica que el header X-WP-TotalPages esté presente (en WP REST API nativa sí viene). Si usas un endpoint personalizado, asegúrate de enviar un header con el total o devolverlo en la respuesta.
- Peticiones 400 o 404 al pedir una página alta: Detecta esos códigos y desconecta el observer como fin de contenido.
- Duplicados o saltos de posts: Asegúrate de usar el mismo criterio de orden y filtros en la consulta PHP inicial y en las consultas posteriores vía REST (misma taxonomy, mismo order, mismo posts_per_page).
- Imágenes no aparecen: Si necesitas imagen destacada en la respuesta, añade _embed=true o personaliza la respuesta del endpoint para incluir el URL de la imagen.
Ejemplo completo resumido
Pasos rápidos:
- Añadir el código de encolar en functions.php (ver ejemplo arriba).
- Insertar en la plantilla el contenedor, sentinel y loader (ver HTML ejemplo arriba).
- Pegar el script JS en js/infinite-scroll.js y ajustar la ruta si usaste endpoint personalizado.
- Añadir los estilos básicos y probar en varios viewports.
Consideraciones finales
Infinite scroll mejora la exploración en muchos contextos, pero requiere cuidado con SEO, usabilidad y rendimiento. Implementar la solución con la REST API nativa de WordPress y IntersectionObserver proporciona una base sólida y eficiente. Inspecciona en herramientas de desarrollo las cabeceras de la API (X-WP-TotalPages y X-WP-Total) y ajusta la lógica del cliente según tu configuración (custom post types, taxonomías, campos personalizados).
Recursos útiles
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |