Como paginar comentarios dinámicamente con JavaScript en WordPress

Contents

Introducción

Este tutorial explica cómo paginar comentarios de WordPress dinámicamente usando JavaScript (AJAX/Fetch) y un endpoint REST personalizado que devuelve HTML de los comentarios ya formateados. La idea es mantener la marca visual y la compatibilidad con los callbacks de comentarios de WordPress (como wp_list_comments) pero actualizar la página sin recargar por completo. El resultado: navegación fluida entre páginas de comentarios, soporte para la historia del navegador (back/forward), y carga progresiva para mejorar UX.

Requisitos y consideraciones

  • WordPress 4.7 (para REST API), idealmente 5.x.
  • Acceso al functions.php del tema o plugin propio para registrar endpoints y encolar scripts.
  • Conocimientos básicos de JavaScript moderno (Fetch, Promises) y PHP.
  • Accesibilidad: los enlaces de paginación deben ser focusables y anunciables por lectores de pantalla.
  • Fallback: asegúrate de que la paginación HTML tradicional siga funcionando si JavaScript está deshabilitado.

Enfoque general

  1. Crear un endpoint REST que acepte parámetros: post_id, page y per_page, y que devuelva HTML con la lista de comentarios y datos de paginación (total, total_pages).
  2. Encolar un script JavaScript en el frontend que capture los clics en los enlaces de paginación y haga llamadas Fetch al endpoint, reemplace el contenedor de comentarios y actualice la URL con history.pushState.
  3. Agregar manejo de estados (cargando, error) y restaurar el estado al navegar con back/forward (window.onpopstate).
  4. Opcional: añadir caching simple en el cliente y optimizaciones en el servidor (transients) para evitar consultas repetidas.

1) Código del endpoint REST (PHP)

Registrar un endpoint en REST API que reciba parámetros y devuelva un array JSON con html, page, total_pages y total_comments. Este endpoint renderizará los comentarios con wp_list_comments para mantener el markup del tema.

 GET,
        callback => custom_rest_get_comments_html,
        permission_callback => __return_true, // ajustar si es necesario
    ))
})

function custom_rest_get_comments_html(WP_REST_Request request) {
    post_id   = absint(request->get_param(post_id))
    page      = max(1, intval(request->get_param(page) ?: 1))
    per_page  = max(1, intval(request->get_param(per_page) ?: get_option(comments_per_page, 10)))
    order     = sanitize_text_field(request->get_param(order) ?: get_option(comment_order, asc))

    if (!get_post(post_id)) {
        return new WP_Error(invalid_post, Post not found, array(status => 404))
    }

    // Obtener total de comentarios aprobados para ese post
    count_args = array(
        post_id => post_id,
        status  => approve,
        count   => true,
    )
    total_comments = get_comments(count_args)
    total_pages = (int) ceil(total_comments / per_page)

    // Obtener comentarios para la página solicitada
    comments_args = array(
        post_id => post_id,
        status  => approve,
        number  => per_page,
        offset  => (page - 1)  per_page,
        orderby => comment_date_gmt,
        order   => order,
    )
    comments = get_comments(comments_args)

    // Renderizar los comentarios usando el walker del tema
    ob_start()
    // El container se puede personalizar en el tema aquí usamos wp_list_comments
    // Asegúrate que tu tema tiene el callback correcto para wp_list_comments
    wp_list_comments(array(style => ol, short_ping => true), comments)
    comments_html = ob_get_clean()

    // Generar HTML para la paginación (links simples) el JS puede procesarlos
    base = add_query_arg(array(page => %#%), get_permalink(post_id))
    pagination_html = paginate_links(array(
        base      => base,
        format    => ?page=%#%,
        current   => page,
        total     => total_pages,
        type      => list,
        end_size  => 1,
        mid_size  => 2,
    ))

    return rest_ensure_response(array(
        html            => comments_html,
        pagination_html => pagination_html,
        page            => page,
        total_pages     => total_pages,
        total_comments  => total_comments,
    ))
}
?>

Notas sobre el endpoint

  • La función wp_list_comments utiliza el callback de tu tema para renderizar cada comentario. Si tu tema no lo define, WordPress usa el por defecto.
  • Se puede mejorar la seguridad restringiendo permission_callback o validando nonces si devuelve datos sensibles.
  • Si prefieres devolver datos crudos (JSON) en lugar de HTML, el cliente debe renderizar los comentarios aquí optamos por HTML para preservar markup complejo.

2) Encolar scripts y pasar variables al cliente

Encolar el archivo JS y pasar las variables necesarias (endpoint URL, post ID, nonce). Usa wp_localize_script o wp_add_inline_script para inyectar configuración.

 esc_url_raw(rest_url(custom/v1/comments)),
        nonce   => wp_create_nonce(wp_rest),
        per_page=> (int) get_option(comments_per_page, 10),
    ))
})
?>

3) Código JavaScript: lógica del cliente

El JS captura clics en enlaces de paginación dentro de un contenedor (por ejemplo #comments-pagination) y solicita la nueva página al endpoint. También maneja history.pushState y window.onpopstate para navegación con botones del navegador.

// Archivo: js/comments-pager.js
(function () {
  if (!(fetch in window)) return // Fallback simple

  var settings = window.CommentsPagerSettings  {}
  var apiUrl = settings.api_url
  var perPage = settings.per_page  10
  var commentsContainer = document.getElementById(comments) // debe existir en tema
  var paginationContainer = document.getElementById(comments-pagination) // donde están los links
  var currentPostId = commentsContainer ? commentsContainer.getAttribute(data-post-id) : null

  if (!commentsContainer  !paginationContainer  !currentPostId) return

  function buildUrl(page) {
    var url = new URL(apiUrl, window.location.origin)
    url.searchParams.set(post_id, currentPostId)
    url.searchParams.set(page, page)
    url.searchParams.set(per_page, perPage)
    return url.toString()
  }

  function setLoading(state) {
    if (state) {
      commentsContainer.classList.add(loading)
      // puedes insertar un spinner si quieres
    } else {
      commentsContainer.classList.remove(loading)
    }
  }

  function fetchPage(page, pushState) {
    setLoading(true)
    fetch(buildUrl(page), {
      method: GET,
      credentials: same-origin,
      headers: {
        X-WP-Nonce: settings.nonce  
      }
    })
    .then(function (res) {
      if (!res.ok) throw new Error(Error en la solicitud:    res.status)
      return res.json()
    })
    .then(function (data) {
      // Reemplazar HTML de comentarios
      commentsContainer.innerHTML = 
    (data.html )
// Reemplazar paginación if (data.pagination_html) { paginationContainer.innerHTML = data.pagination_html } else { paginationContainer.innerHTML = } // Actualizar el estado de la URL (query param cpage por ejemplo) var newUrl = new URL(window.location.href) newUrl.searchParams.set(cpage, page) if (pushState) { history.pushState({ cpage: page }, , newUrl.toString()) } else { history.replaceState({ cpage: page }, , newUrl.toString()) } // Opcional: foco en container para a11y commentsContainer.setAttribute(tabindex, -1) commentsContainer.focus() }) .catch(function (err) { console.error(err) // mostrar mensaje de error mínimo paginationContainer.insertAdjacentHTML(beforeend,

No se pudieron cargar los comentarios. Inténtalo de nuevo.

) }) .finally(function () { setLoading(false) }) } // Manejar clics delegados en el contenedor de paginación paginationContainer.addEventListener(click, function (e) { var target = e.target // Buscar el enlace hasta el ancestor while (target target !== paginationContainer target.tagName !== A) target = target.parentNode if (!target target === paginationContainer) return e.preventDefault() // Determinar página objetivo desde href o texto var href = target.getAttribute(href) var pageMatch = null if (href) { try { var u = new URL(href, window.location.origin) pageMatch = u.searchParams.get(page) u.searchParams.get(cpage) } catch (err) { // parse fallback: buscar ?page=# var m = href.match(/page=(d )/) if (m) pageMatch = m[1] } } var page = parseInt(pageMatch, 10) 1 fetchPage(page, true) }) // Manejar back/forward window.addEventListener(popstate, function (e) { var state = e.state var page = state state.cpage ? state.cpage : (new URL(window.location.href)).searchParams.get(cpage) 1 fetchPage(page, false) }) // Inicial: si URL tiene cpage, cargar esa página var initialPage = (new URL(window.location.href)).searchParams.get(cpage) 1 // Si la página inicial no es 1, solicitarla para asegurar que el HTML coincide if (parseInt(initialPage, 10) !== 1) { fetchPage(initialPage, false) } })()

Explicación de decisiones en JS

  • Uso de fetch con credentials para soportar cookies si es necesario.
  • Encapsulado y delegación de eventos para funcionamiento con paginaciones generadas dinámicamente.
  • Uso de history.pushState para actualizar la URL sin recargar y window.onpopstate para restaurar páginas al usar atrás/adelante.
  • Foco en el contenedor tras el cambio para accesibilidad.

4) Ejemplo de HTML del tema (marcadores para JS)

Tu plantilla de comentarios (comments.php o template part) debe ofrecer contenedores con identificadores utilizados por el script. Un ejemplo sencillo:


>

5) Estilos básicos (CSS)

Agregar un estilo de carga y visuales mínimos para indicar que se está cargando.

/ Ejemplo simple /
#comments.loading { opacity: 0.6 position: relative }
#comments.loading:after {
  content: Cargando...
  position: absolute
  right: 1rem
  top: 1rem
  background: rgba(0,0,0,0.6)
  color: #fff
  padding: 0.2rem 0.5rem
  border-radius: 3px
  font-size: 0.9rem
}
.comments-error { color: #c00 margin-top: 1rem }

6) Mejores prácticas y optimizaciones

  • Cache en servidor: si tu site recibe muchas peticiones, considera usar transients para almacenar HTML de páginas de comentarios por X segundos.
  • Evita consultas innecesarias: limita metadatos cargados por comentario si no los necesitas.
  • Paginación accesible: usa enlaces ltagt con texto claro (Página 2) y atributos ARIA si procede.
  • SEO y enlaces profundos: actualizar la URL permite enlazar a una página de comentarios concreta. También añade rel=prev/next si generas links en servidor.
  • Fallback sin JS: conserva la paginación tradicional en HTML para usuarios sin JavaScript.
  • Protección contra scraping: si expones datos sensibles o emails en comentarios, sanitiza y evita exponer información no permitida.

7) Manejo de errores comunes

  1. 500 o error REST: revisa logs PHP y confirma que el endpoint se registró correctamente y no hay errores de sintaxis.
  2. Comentarios vacíos: si no hay comentarios, asegúrate de que la respuesta devuelva HTML vacío y la UI muestre un mensaje amigable.
  3. Problemas con el markup: si el tema usa un walker personalizado, verifica que wp_list_comments esté recibiendo los argumentos correctos y que el markup resultante sea válido.
  4. Nonce incorrecto: si obtienes 403, revisa que estás enviando el header X-WP-Nonce y que se crea con wp_create_nonce(wp_rest).

8) Extensiones y variaciones posibles

  • Infinite scroll: en lugar de paginación, cargar la siguiente página automáticamente al llegar al final.
  • Renderizado parcial: devolver solo datos JSON y construir cada comentario en el cliente (útil para apps SPA o cuando necesitas reuso extremo).
  • Filtrado y orden: permitir parámetros de búsqueda, autor o orden en el endpoint para funcionalidad avanzada.
  • Comentarios anidados: si tu tema utiliza comentarios anidados profundos, asegúrate de que wp_list_comments preserve la estructura.

Conclusión

La paginación dinámica de comentarios en WordPress mejora significativamente la experiencia del usuario manteniendo la semántica y el markup original del tema cuando el servidor devuelve HTML. Con un endpoint REST personalizado, un script cliente que gestione Fetch y pushState, y un fallback HTML tradicional, obtendrás una solución robusta, accesible y amigable para SEO. Implementa cache en servidor y validaciones de seguridad según sea necesario para cargas altas o requisitos específicos.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *