Como evitar recuento doble de vistas con cookies en JS en WordPress

Contents

Cómo evitar el recuento doble de vistas en WordPress usando cookies y JavaScript (tutorial detallado)

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.

2) Lógica en JavaScript (crear cookie global, cookie por post y AJAX)

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

  1. Abre DevTools gt Application gt Cookies para revisar que pv_uid y pv_post_{ID} se crean correctamente y con la expiración deseada.
  2. En Network revisa la petición POST a admin-ajax.php y su respuesta JSON.
  3. Prueba recargando varias veces: la primera carga debe incrementar el contador las siguientes, si la cookie por post existe, no deben hacerlo.
  4. Prueba en otra pestaña o en otro navegador para verificar que cada navegador es contado independientemente.
  5. 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 🙂



Deja una respuesta

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