Contents
Contar vistas de entradas en WordPress puede parecer trivial, pero en sitios reales aparecen múltiples problemas: recuentos dobles por recargas, múltiples pestañas, sesiones de incógnito, bots, caching y más. En este tutorial se propone una solución práctica, segura y escalable basada en cookies y una verificación adicional en el servidor para minimizar recuentos duplicados. Incluiré código listo para usar (JavaScript y PHP), consideraciones de seguridad, privacidad y alternativas.
Resumen de la estrategia
- Crear un identificador único de visitante (cookie global) para distinguir navegadores/usuarios.
- Marcar a nivel de post cuando ese visitante ya ha contabilizado la vista (cookie por post) para evitar doble conteo desde el mismo navegador.
- Al primer acceso que debe contarse, enviar una petición AJAX al servidor para incrementar la vista.
- En el servidor, verificar un nonce y mantener una lista temporal (transient) de identificadores únicos por post para prevenir duplicados aún si el cliente intenta manipular la cookie.
- Respetar políticas de privacidad: no almacenar datos personales y respetar la aceptación de cookies si aplica.
Ventajas de este enfoque
- Funciona con caches de página (la petición AJAX es dinámica).
- Reduce recuentos por recargas y múltiples pestañas del mismo navegador.
- Servidor valida y limita duplicados mediante transients, minimizando dependencia exclusiva del cliente.
- Configuración flexible: ventana de tiempo para contar una nueva vista (por ejemplo 24 horas).
Limitaciones y casos especiales
- Vistas desde navegadores en modo incógnito se cuentan como nuevos visitantes (cookies separadas).
- Si el usuario borra cookies, volverá a contarse como nuevo visitante.
- Si quieres evitar cualquier fraude sofisticado (bots que aceptan cookies, proxies, etc.) necesitarás mecanismos adicionales (rate limiting por IP, análisis heurístico, servicios antispam).
- Cumplimiento legal: si usas cookies que no son estrictamente necesarias, debes pedir consentimiento (GDPR/CCPA).
Implementación paso a paso
1) Encolar el script y pasar datos desde PHP
En functions.php (o en tu plugin) encola un script que maneje la lógica del cliente y pasa los valores necesarios (ajax_url, nonce, post_id y la duración de la cookie en segundos).
lt?php // En functions.php o en tu plugin function pv_enqueue_post_views_script() { if ( is_singular() ) { // solo en páginas de entrada individual wp_enqueue_script( post-views, get_stylesheet_directory_uri() . /js/post-views.js, array(), // no dependencias obligatorias, usa fetch 1.0, true ) global post wp_localize_script(post-views, PostViewsData, array( ajax_url =gt admin_url(admin-ajax.php), nonce =gt wp_create_nonce(post-views-nonce), post_id =gt intval(post-gtID), cookie_exp =gt DAY_IN_SECONDS // ejemplo: 1 día (86400) )) } } add_action(wp_enqueue_scripts, pv_enqueue_post_views_script) ?gt
Nota: si tu tema incrusta el script solo en single.php, también puedes imprimir el post ID como un data-attribute en el HTML y leerlo desde JS. En este ejemplo usamos wp_localize_script para comodidad.
El script comprueba si existe una cookie global de visitante (pv_uid) si no existe crea una. Luego, para el post actual, comprueba una cookie de bloqueo por post (pv_post_{ID}). Si esa cookie no existe envía la petición AJAX al servidor con el pv_uid y el post_id cuando el servidor responde con éxito, se crea la cookie por post para no volver a contar desde ese navegador dentro del periodo configurado.
// archivo: js/post-views.js (function() { const postId = typeof PostViewsData !== undefined ? PostViewsData.post_id : null const ajaxUrl = PostViewsData.ajax_url const nonce = PostViewsData.nonce const cookieExp = PostViewsData.cookie_exp 86400 // segundos if (!postId) return // Funciones de cookie simples function setCookie(name, value, seconds) { let expires = if (seconds) { const d = new Date() d.setTime(d.getTime() (seconds 1000)) expires = expires= d.toUTCString() } const secure = location.protocol === https: ? Secure : document.cookie = name = encodeURIComponent(value) expires path=/ secure SameSite=Lax } function getCookie(name) { const v = document.cookie.match((^)s name s=s([^] )) return v ? decodeURIComponent(v.pop()) : null } function createUid() { // UID simple: timestamp random. No datos personales. return pv_ Date.now().toString(36) Math.random().toString(36).substr(2,9) } // Asegurar cookie global pv_uid let visitorId = getCookie(pv_uid) if (!visitorId) { visitorId = createUid() // larga duración por defecto (1 año) setCookie(pv_uid, visitorId, 31536000) } // Cookie específica por post para evitar recontar desde el mismo navegador const postCookieName = pv_post_ postId if (getCookie(postCookieName)) { // ya contada desde este navegador dentro del periodo return } // Enviar la petición para contar la vista fetch(ajaxUrl, { method: POST, credentials: same-origin, // enviar cookies headers: { Content-Type: application/x-www-form-urlencoded charset=UTF-8 }, body: new URLSearchParams({ action: increment_post_views, nonce: nonce, post_id: postId, pv_uid: visitorId }) }) .then(function(response) { return response.json() }) .then(function(json) { if (json json.success) { // marcar que ya contamos esta vista en este navegador setCookie(postCookieName, 1, cookieExp) } else { // no se incrementó (posible duplicado o error). No crear la cookie por post. // Podrías registrar para depuración. } }) .catch(function(err) { // fallo en la petición - dejar sin cookie para reintentar en recarga // console.error(err) }) })()
3) Handler PHP para admin-ajax.php (validación y servidor)
En PHP validamos el nonce, sanitizamos el post_id y el pv_uid. Para evitar el doble conteo si un cliente manipula cookies, mantenemos un transient por post que guarda un array de hashes de pv_uids que ya han contado durante la ventana temporal (por ejemplo 24 horas).
lt?php // En functions.php o plugin add_action(wp_ajax_nopriv_increment_post_views, pv_ajax_increment_post_views) add_action(wp_ajax_increment_post_views, pv_ajax_increment_post_views) function pv_ajax_increment_post_views() { // Verificar nonce nonce = isset(_POST[nonce]) ? sanitize_text_field(_POST[nonce]) : if ( ! wp_verify_nonce(nonce, post-views-nonce) ) { wp_send_json_error(nonce_invalid, 403) } post_id = isset(_POST[post_id]) ? intval(_POST[post_id]) : 0 if ( post_id <= 0 get_post(post_id) === null ) { wp_send_json_error(invalid_post, 400) } // pv_uid enviado por cliente (no se confía ciegamente, se hashea) pv_uid = isset(_POST[pv_uid]) ? sanitize_text_field(_POST[pv_uid]) : if ( empty(pv_uid) ) { wp_send_json_error(no_uid, 400) } // Configuración: duración en segundos (debe coincidir con cookie en JS) cookie_exp = defined(DAY_IN_SECONDS) ? DAY_IN_SECONDS : 86400 transient_key = pv_keys_ . post_id // Obtener array de hashes de visitantes que ya han contado recientemente seen = get_transient(transient_key) if ( ! is_array(seen) ) { seen = array() } // Hashear el pv_uid con una sal para no guardar el UID literal salt = pv_salt_2025 // cambia/gestiona en tu entorno hash = hash(sha256, pv_uid . salt) // Si ya existe el hash, no contar if ( in_array(hash, seen, true) ) { wp_send_json_success(array(counted => false, reason => already_counted)) } // Incrementar el contador de vistas del post (postmeta) meta_key = post_views_count current = intval(get_post_meta(post_id, meta_key, true)) new = current 1 update_post_meta(post_id, meta_key, new) // Añadir hash al array y volver a guardar transient seen[] = hash // Para evitar crecimiento indefinido, podriamos limitar el tamaño: if ( count(seen) gt 5000 ) { // Mantener solamente los últimos 5000 seen = array_slice(seen, -3000) } set_transient(transient_key, seen, cookie_exp) wp_send_json_success(array(counted => true, new_total => new)) } ?gt
Explicación breve:
- Se usa get_transient/set_transient para mantener la lista de identificadores por post solo durante la ventana (cookie_exp).
- Los pv_uids son hasheados antes de almacenarse para no guardar identificadores en claro.
- Se actualiza meta post_views_count. En sitios de alto tráfico, para evitar condiciones de carrera intensas, puedes usar consultas directas a la base de datos y transacciones, o sistemas externos (Redis) para contadores atómicos.
4) Variantes y mejoras
- Usar la REST API: en lugar de admin-ajax.php puedes exponer un endpoint REST con register_rest_route es más moderno y puede integrarse mejor con caches y CDNs.
- Almacenamiento en tabla propia: si necesitas auditoría o muchos checks, crea una tabla para vistas (post_id, hashed_uid, timestamp) en vez de transients. Así puedes realizar consultas históricas, limpiar entradas antiguas y evitar límite del transient system.
- Rate limiting: guarda también la IP y el timestamp en logs temporales para detectar patrones de abuso (ten cuidado con la privacidad).
- Ignorar ciertos roles/UA: por ejemplo no contar administradores o ciertos bots según User-Agent.
- Cookie consent: si el visitante no ha dado consentimiento, no crear pv_uid ni cookies en su lugar puedes contar solo con IP heurística o no contar vistas hasta el consentimiento.
5) Consideraciones de seguridad y rendimiento
- Validar nonce para impedir peticiones CSRF.
- Sanitizar y validar todo input del cliente.
- Los transients son adecuados para ventanas temporales limpialos periódicamente si usas tamaños grandes.
- Si tu sitio recibe mucho tráfico, la lista en transient puede crecer y consumir memoria. Evalúa usar una tabla dedicada o un almacén en memoria (Redis) y un contador atómico.
- No confíes únicamente en la cookie del cliente para seguridad — siempre valida en servidor.
6) Pruebas y depuración
- Abre DevTools gt Application gt Cookies para revisar que pv_uid y pv_post_{ID} se crean correctamente y con la expiración deseada.
- En Network revisa la petición POST a admin-ajax.php y su respuesta JSON.
- Prueba recargando varias veces: la primera carga debe incrementar el contador las siguientes, si la cookie por post existe, no deben hacerlo.
- Prueba en otra pestaña o en otro navegador para verificar que cada navegador es contado independientemente.
- Prueba borrando cookies para comprobar que vuelve a contar como nuevo visitante.
7) Alternativa rápida: solo localStorage
Si no quieres usar cookies por política de privacidad, puedes usar localStorage para marcar la vista por post. Limitación: localStorage no se envía al servidor y es específico del navegador y protocolo. Además, localStorage no caduca automáticamente deberás guardar timestamps para expirar entradas manualmente.
// Ejemplo simple con localStorage (no recomendado si quieres validación servidor fuerte) const key = pv_post_ postId const stored = localStorage.getItem(key) if (!stored) { // enviar AJAX ... localStorage.setItem(key, Date.now()) }
Conclusión y buenas prácticas
La combinación de una cookie global de visitante, una cookie específica por post y una verificación en servidor (transients con hashes) ofrece un balance razonable entre precisión, privacidad y rendimiento para evitar recuentos dobles de vistas en WordPress. Ajusta la ventana temporal y la estrategia de almacenamiento según el tráfico y requisitos de privacidad de tu sitio.
Recomendaciones de configuración:
- Cookie global pv_uid con larga caducidad (ej. 1 año).
- Cookie por post con la ventana que determines (ej. 24 horas para “visitas diarias” o 1 hora para menos restricción).
- Usar transients por post con la misma duración para validar en servidor.
- Si el tráfico es muy alto, evaluar tablas dedicadas o Redis para contadores y listas de IDs.
Implementación lista para producción
El código aquí presentado es una base robusta y práctica. Antes de pasar a producción revisa: políticas de privacidad y cookies del sitio, rendimiento en condiciones de carga, y la posibilidad de que plugins de caché bloqueen admin-ajax.php (si es el caso, considera REST API o endpoints personalizados).
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |