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