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 🙂 |
