Créa-blog

#100JoursPourCoder
Projet Créa-code

Ressources pour développeur web

Théme de la semaine : Découvrir node.js

Codez un plugin WordPress pour compter les vues d’articles

Temps de lecture estimé : 18 minutes
Accueil PHP 8 Codez un plugin WordPress pour compter les vues d’articles

Vous souhaitez savoir combien de visiteurs consultent vos articles sur votre blog WordPress ? Avoir cette information est très utile pour comprendre quel contenu attire le plus vos lecteurs et adapter votre stratégie éditoriale. Si vous êtes développeur ou simplement passionné de WordPress, vous avez sans doute déjà pensé à installer un plugin externe. Cependant, créer votre propre plugin pour connaitre les stats des vues des articles vous offre plusieurs avantages : vous contrôlez entièrement les données, vous pouvez l’optimiser pour votre site, et vous apprendrez beaucoup sur le fonctionnement de WordPress.

Dans ce tutoriel, nous allons coder ensemble un plugin WordPress capable de compter le nombre de vues de chaque article. Ce plugin sera simple, léger et efficace.

Il vous permettra de visualiser les statistiques dans le tableau de bord WordPress, d’exclure les administrateurs et les robots des statistiques et d’afficher le nombre de visiteurs actifs et sur quelles pages ils se trouvent. Nous allons détailler chaque fichier nécessaire à la création de ce plugin, expliquer leur rôle et le code qu’ils contiennent, tout en utilisant un langage clair, compréhensible par tous.

Avant de commencer, il est important de rappeler que créer un plugin WordPress ne nécessite pas de connaissances avancées en PHP, mais il faut avoir quelques notions de base en développement web, notamment en PHP, HTML, CSS et MySQL. Ce tutoriel est pensé pour que chacun puisse suivre pas à pas, même sans expérience préalable dans le développement de plugins.

Plugin WordPress de stats des vues des articles

Téléchargez ce Plugin WordPress pour les stats de vues des articles.

Présentation générale du plugin WordPress pour comptabiliser le nombre des vues des articles

Notre plugin sera organisé selon une structure bien définie afin de faciliter sa maintenance et son évolution. Voici l’arborescence générale que nous allons créer :

wp-content/plugins/wp-stats-visites/
├─ wp-stats-visites.php
├─ uninstall.php
├─ includes/
│  ├─ class-stats-db.php
│  ├─ class-stats-tracker.php
│  └─ class-stats-admin.php
├─ assets/
│  └─ admin.css
└─ readme.txt

Chaque fichier a un rôle précis :

  • stats-visites.php : c’est le fichier principal du plugin. Il sert à déclarer le plugin, à inclure les fichiers nécessaires et à initialiser le fonctionnement.
  • uninstall.php : ce fichier permet de supprimer proprement la base de données créée par le plugin lors de sa désinstallation.
  • class-stats-db.php : cette classe gère la création et la structure de la base de données, en créant les tables pour stocker les vues des articles et les sessions des visiteurs.
  • class-stats-tracker.php : cette classe se charge de suivre les visiteurs sur le site, de compter les vues et de s’assurer que les administrateurs ou les robots ne sont pas comptabilisés.
  • class-stats-admin.php : cette classe permet d’afficher les statistiques dans le tableau de bord WordPress, avec un design simple et efficace, des filtres pour trier les données et une pagination.
  • admin.css : ce fichier contient les styles CSS pour améliorer l’affichage des statistiques dans l’interface d’administration.
  • readme.txt : ce fichier contient des informations sur le plugin, comme son nom et sa description.

En suivant cette organisation, le plugin restera propre, léger et facile à comprendre.

Création du fichier principal : stats-visites.php

Le fichier principal de votre plugin WordPress est le point d’entrée. Il doit être placé directement dans le dossier wp-content/plugins/stats-visites/. Ce fichier contient les informations que WordPress utilise pour identifier votre plugin, telles que son nom, sa version et l’auteur. Il sert également à inclure toutes les classes dont nous aurons besoin.

Voici le code complet de stats-visites.php :

<?php
/**
 * Plugin Name: WP Stats Articles
 * Description: Statistiques de visites des articles.
 * Version: 1.0.0
 * Author: GUILLIER Alban
 * Text Domain: WP-stats-visites
 * Author URI:  https://blog.crea-troyes.fr
 * License:     GPL2
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Plugin URI:  https://github.com/tonpseudo/wp-article-visitor-counter
 * Plugin URI:  https://github.com/crea-troyes/wp-stats-articles
 */

if ( ! defined( 'ABSPATH' ) ) exit;

define( 'STATS_VISITES_DIR', plugin_dir_path( __FILE__ ) );
define( 'STATS_VISITES_URL', plugin_dir_url( __FILE__ ) );

require_once STATS_VISITES_DIR . 'includes/class-stats-db.php';
require_once STATS_VISITES_DIR . 'includes/class-stats-tracker.php';
require_once STATS_VISITES_DIR . 'includes/class-stats-admin.php';

register_activation_hook( __FILE__, array( 'Stats_DB', 'activate' ) );
register_deactivation_hook( __FILE__, array( 'Stats_DB', 'deactivate' ) );

add_action( 'plugins_loaded', function() {
    Stats_Tracker::init();
    Stats_Admin::init();
} );

Explication du code

Tout d’abord, nous commençons par déclarer le plugin avec un commentaire PHP contenant le nom, la description, la version et l’auteur. Ces informations seront visibles dans l’interface d’administration de WordPress, dans la liste des plugins.

Ensuite, nous vérifions si la constante ABSPATH est définie. Cette vérification permet d’éviter que quelqu’un n’accède directement au fichier depuis un navigateur. C’est une mesure de sécurité simple mais importante.

Puis, nous définissons deux constantes :

  • STATS_VISITES_DIR : qui correspond au chemin complet du dossier du plugin sur le serveur.
  • STATS_VISITES_URL : qui correspond à l’URL du dossier du plugin, utile pour charger des fichiers CSS ou JavaScript.

Ensuite, nous incluons les trois classes essentielles du plugin : class-stats-db.phpclass-stats-tracker.php et class-stats-admin.php. Ces classes contiennent respectivement la logique de la base de données, le suivi des visites et l’affichage dans le tableau de bord.

Nous utilisons ensuite deux hooks importants de WordPress : register_activation_hook et register_deactivation_hook. Ces hooks permettent d’exécuter des fonctions spécifiques lors de l’activation ou de la désactivation du plugin. Ici, nous allons utiliser ces hooks pour créer la base de données lors de l’activation, et pour éventuellement nettoyer certains paramètres à la désactivation.

Enfin, nous ajoutons une action sur plugins_loaded pour initialiser notre suivi des visites et notre interface d’administration. Cela garantit que tout est prêt dès que WordPress a chargé tous les plugins.

Créer la base de données pour votre plugin WordPress

Pour suivre les visites de vos articles, il est essentiel de stocker des informations dans une base de données. Chaque fois qu’un visiteur consulte un article, nous allons enregistrer certaines informations comme l’ID de l’article, l’heure de la visite et un identifiant unique pour le visiteur. Ces données seront ensuite utilisées pour générer des statistiques précises dans le tableau de bord WordPress.

Le plugin WordPress que nous créons doit être performant et léger. Nous devons donc concevoir la base de données de manière à ne pas ralentir votre site, tout en permettant de récupérer facilement les statistiques.

Notre plugin utilisera trois tables principales :

  1. stats_post_views : pour enregistrer chaque vue d’article.
  2. stats_post_totals : pour stocker le total des vues par article, ce qui permet de faire un tri rapide sans calculer toutes les lignes de stats_post_views.
  3. stats_sessions : pour suivre les visiteurs actifs sur le site, en excluant les administrateurs et les robots.

Cette structure permet de séparer les données historiques (chaque vue) et les données agrégées (totaux et sessions), ce qui optimise les performances et simplifie l’affichage des statistiques.

Le fichier class-stats-db.php

Ce fichier se trouve dans le dossier includes/ du plugin et contient la classe Stats_DB. Cette classe est responsable de la création et de la gestion des tables de la base de données. Elle est également appelée lors de l’activation du plugin pour créer automatiquement les tables nécessaires.

Voici le code complet du fichier :

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

class Stats_DB {
    public static function activate() {
        global $wpdb;
        $charset_collate = $wpdb->get_charset_collate();
        $prefix = $wpdb->prefix;

        $sql = [];

        // table : post views (historique pour filtres)
        $sql[] = "CREATE TABLE {$prefix}stats_post_views (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            post_id BIGINT UNSIGNED NOT NULL,
            viewed_at DATETIME NOT NULL,
            ip_hash VARCHAR(64) NOT NULL,
            user_agent TEXT,
            PRIMARY KEY (id),
            INDEX (post_id),
            INDEX (viewed_at)
        ) $charset_collate;";

        // table : totaux (optimisation pour tri)
        $sql[] = "CREATE TABLE {$prefix}stats_post_totals (
            post_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
            view_count BIGINT UNSIGNED NOT NULL DEFAULT 0
        ) $charset_collate;";

        // table : sessions / visiteurs actifs
        $sql[] = "CREATE TABLE {$prefix}stats_sessions (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            ip_hash VARCHAR(64) NOT NULL,
            last_activity DATETIME NOT NULL,
            page VARCHAR(191) DEFAULT NULL,
            post_id BIGINT UNSIGNED DEFAULT NULL,
            user_agent TEXT,
            PRIMARY KEY (id),
            UNIQUE KEY ip_hash_unique (ip_hash)
        ) $charset_collate;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );

        add_option( 'stats_visites_db_version', '1.0' );
    }

    public static function deactivate() {
        // pas de suppression automatique
    }
}

Explications détaillées du code

if ( ! defined( 'ABSPATH' ) ) exit;

Cette ligne empêche toute exécution directe du fichier dans le navigateur. Cela sécurise le plugin contre d’éventuelles attaques.

La classe Stats_DB

Nous définissons une classe Stats_DB avec deux méthodes principales :

  • activate() : exécutée lors de l’activation du plugin, elle crée toutes les tables nécessaires.
  • deactivate() : exécutée lors de la désactivation du plugin, elle pourrait supprimer ou nettoyer certaines options. Dans notre cas, nous n’avons pas besoin de supprimer les données automatiquement, car cela pourrait entraîner une perte d’informations précieuses.

Création des tables

Nous utilisons la variable $wpdb pour interagir avec la base de données WordPress et $wpdb->prefix pour récupérer le préfixe des tables, généralement wp_. Cela garantit la compatibilité avec tous les sites WordPress, même ceux ayant un préfixe personnalisé.

  1. stats_post_views : chaque vue d’article est enregistrée avec :
  • post_id : l’identifiant de l’article.
  • viewed_at : la date et l’heure de la visite.
  • ip_hash : un hash de l’adresse IP du visiteur pour protéger la vie privée.
  • user_agent : l’agent utilisateur du navigateur, utile pour détecter les robots.

Cette table contient également des index sur post_id et viewed_at pour accélérer les requêtes.

  1. stats_post_totals : cette table stocke le nombre total de vues par article. Cela permet d’afficher rapidement les articles les plus lus sans compter toutes les lignes de stats_post_views à chaque affichage.
  2. stats_sessions : cette table suit les visiteurs actifs. Chaque visiteur est identifié par un ip_hash unique. Nous enregistrons également la dernière activité, la page consultée et l’ID du post. Cela permet de générer la liste des visiteurs actifs sur le tableau de bord.

Création sécurisée des tables avec dbDelta

WordPress fournit la fonction dbDelta() qui compare la structure des tables existantes avec celle que vous définissez et crée ou met à jour les tables en conséquence. Cela évite d’écraser les données existantes lors de la mise à jour du plugin.

Stockage de la version

add_option( 'stats_visites_db_version', '1.0' );

Nous enregistrons la version de la base de données dans les options de WordPress. Cela permettra, à l’avenir, de gérer facilement les mises à jour ou les modifications de la structure des tables.

Formation web et informatique - Alban Guillier - Formateur

Des formations informatique pour tous !

Débutant ou curieux ? Apprenez le développement web, le référencement, le webmarketing, la bureautique, à maîtriser vos appareils Apple et bien plus encore…

Formateur indépendant, professionnel du web depuis 2006, je vous accompagne pas à pas et en cours particulier, que vous soyez débutant ou que vous souhaitiez progresser. En visio, à votre rythme, et toujours avec pédagogie.

Découvrez mes formations Qui suis-je ?

Bonnes pratiques pour les bases de données WordPress

Lorsque vous créez un plugin, il est important de respecter certaines bonnes pratiques :

  • Toujours utiliser $wpdb->prefix pour le nom des tables.
  • Ajouter des index sur les colonnes souvent utilisées pour améliorer les performances.
  • Protéger les informations personnelles des utilisateurs (par exemple, en stockant un hash de l’adresse IP plutôt que l’IP en clair).
  • Ne jamais exécuter de requêtes SQL directement sans préparation si vous utilisez des valeurs externes. Préférez $wpdb->prepare() pour éviter les injections SQL.

Avec cette étape, votre plugin WordPress est capable de créer automatiquement toutes les tables nécessaires pour enregistrer les vues et les sessions des visiteurs. C’est une base solide pour construire la suite du plugin, notamment le suivi réel des visiteurs et l’affichage des statistiques.

Suivre les visites et les visiteurs actifs

Pour qu’un plugin WordPress de statistiques soit utile, il ne suffit pas de compter le nombre de visiteurs. Il faut également :

  • Ne pas compter les visites des administrateurs pour éviter de fausser les statistiques.
  • Ne pas inclure les robots ou crawlers, comme Googlebot ou Bingbot.
  • Enregistrer les visites de manière fiable pour pouvoir générer des rapports précis.
  • Identifier les visiteurs actifs sur le site et savoir sur quelle page ils se trouvent.

La classe Stats_Tracker est dédiée à toutes ces tâches. Elle interagit directement avec la base de données que nous avons créée précédemment pour stocker chaque vue et mettre à jour les sessions des visiteurs actifs.

Le fichier class-stats-tracker.php

Ce fichier se trouve dans le dossier includes/ et contient la classe Stats_Tracker. Voici le code complet :

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

class Stats_Tracker {
    protected static $bot_patterns = [
        // moteurs de recherche classiques
    'googlebot', 'bingbot', 'slurp', 'yahoo', 'yandex', 'duckduckbot', 
    'baiduspider', 'sogou', 'exabot', 'facebot', 'facebookexternalhit', 
    'mediapartners-google', 'bingpreview', 'seznambot',

    // bots SEO et analyse
    'ahrefsbot', 'semrushbot', 'mj12bot', 'dotbot', 'majestic12', 'rogerbot', 
    'blekkobot', 'sitebot', 'crawler', 'spider', 'robot',

    // scrapers et outils divers
    'curl', 'wget', 'python-requests', 'httpclient', 'libwww-perl', 'java', 'node-fetch', 'ruby', 'php', 'perl', 'scrapy', 'go-http-client',

    // réseaux sociaux
    'twitterbot', 'linkedinbot', 'pinterest', 'slackbot', 'telegrambot',

    // autres bots fréquents
    'discordbot', 'applebot', 'embedly', 'quora link preview', 'ahoy', 'msnbot'
    ];

    public static function init() {
        add_action( 'template_redirect', [ __CLASS__, 'maybe_track' ], 0 );
        add_action( 'init', [ __CLASS__, 'maybe_update_session' ], 0 );
    }

    protected static function is_bot( $ua ) {
        if ( empty( $ua ) ) return true;
        $ua = strtolower( $ua );
        foreach ( self::$bot_patterns as $p ) {
            if ( strpos( $ua, $p ) !== false ) return true;
        }
        return false;
    }

    protected static function ip_hash() {
        $ip = self::get_remote_addr();
        if ( ! $ip ) return '';
        // hash to avoid storing raw IP
        return hash( 'sha256', $ip );
    }

    protected static function get_remote_addr() {
        if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
            return sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) );
        }
        if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
            $arr = explode( ',', wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
            return sanitize_text_field( trim( $arr[0] ) );
        }
        if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
            return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
        }
        return '';
    }

    public static function maybe_track() {
        if ( is_admin() ) return;
        // Only track single posts (articles)
        if ( ! is_singular( 'post' ) ) {
            // still update session for active visitors on non-post pages
            return;
        }

        // Exclude admin users
        if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) {
            return;
        }

        $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : '';
        if ( self::is_bot( $ua ) ) return;

        $post_id = get_queried_object_id();
        if ( ! $post_id ) return;

        global $wpdb;
        $prefix = $wpdb->prefix;
        $ip_hash = self::ip_hash();
        $now = current_time( 'mysql', 0 );

        // insert into history
        $wpdb->insert(
            $prefix . 'stats_post_views',
            [
                'post_id' => $post_id,
                'viewed_at' => $now,
                'ip_hash' => $ip_hash,
                'user_agent' => substr( $ua, 0, 65535 ),
            ],
            [ '%d', '%s', '%s', '%s' ]
        );

        // increment totals (INSERT ... ON DUPLICATE KEY UPDATE)
        $table_tot = $prefix . 'stats_post_totals';
        $wpdb->query(
            $wpdb->prepare(
                "INSERT INTO {$table_tot} (post_id, view_count) VALUES (%d, 1)
                ON DUPLICATE KEY UPDATE view_count = view_count + 1",
                $post_id
            )
        );
    }

    public static function maybe_update_session() {
        // Update sessions for active visitors for any front-end request (not admin)
        if ( is_admin() ) return;

        // Exclude admin users
        if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) {
            return;
        }

        $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : '';
        if ( self::is_bot( $ua ) ) return;

        global $wpdb;
        $prefix = $wpdb->prefix;
        $ip_hash = self::ip_hash();
        if ( empty( $ip_hash ) ) return;

        $now = current_time( 'mysql', 0 );
        $page = esc_url_raw( ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/' ) );
        $post_id = ( is_singular( 'post' ) ? get_queried_object_id() : null );

        // try update existing
        $updated = $wpdb->update(
            $prefix . 'stats_sessions',
            [
                'last_activity' => $now,
                'page' => substr( $page, 0, 190 ),
                'post_id' => $post_id,
                'user_agent' => substr( $ua, 0, 65535 ),
            ],
            [ 'ip_hash' => $ip_hash ],
            [ '%s', '%s', '%d', '%s' ],
            [ '%s' ]
        );

        if ( $updated === false ) return;

        if ( $updated === 0 ) {
            // insert new
            $wpdb->insert(
                $prefix . 'stats_sessions',
                [
                    'ip_hash' => $ip_hash,
                    'last_activity' => $now,
                    'page' => substr( $page, 0, 190 ),
                    'post_id' => $post_id,
                    'user_agent' => substr( $ua, 0, 65535 ),
                ],
                [ '%s', '%s', '%s', '%d', '%s' ]
            );
        }
    }
}

Initialisation du suivi

La méthode init() ajoute deux actions WordPress :

  • template_redirect : déclenchée sur chaque page visitée côté front-end. Elle permet de comptabiliser les vues d’articles.
  • init : déclenchée à chaque chargement de WordPress. Elle est utilisée pour mettre à jour les sessions des visiteurs actifs.

Détection des robots

Nous utilisons un tableau $bot_patterns contenant les motifs les plus connus pour identifier les crawlers. La méthode is_bot($ua) compare l’agent utilisateur de chaque visiteur avec ces motifs. Si un robot est détecté, la visite n’est pas comptée.

Exclusion des administrateurs

if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) return;

Cette condition empêche le comptage des visites des administrateurs, afin que vos propres consultations n’influencent pas les statistiques.

Identification des visiteurs

Chaque visiteur est identifié par un ip_hash généré à partir de son adresse IP. Cela permet de suivre les visites sans stocker directement les IP, respectant ainsi la vie privée.

Comptage des vues d’articles

Si toutes les conditions sont respectées (ni robot, ni admin), la méthode maybe_track() enregistre la visite dans la table stats_post_views. Elle met également à jour la table stats_post_totals pour incrémenter rapidement le compteur total de l’article.

Suivi des sessions actives

La méthode maybe_update_session() maintient la table stats_sessions à jour. Chaque visiteur actif est enregistré avec sa dernière activité et la page consultée. Si le visiteur existe déjà dans la table, son enregistrement est mis à jour, sinon un nouvel enregistrement est créé. Cette méthode permet de visualiser les visiteurs actifs en temps réel dans l’interface d’administration.

Sécurité et performance

  • Toutes les données provenant de l’utilisateur sont échappées avec sanitize_text_field() et wp_unslash() pour éviter les injections.
  • La table des sessions utilise un index unique sur ip_hash pour accélérer les mises à jour.
  • Les requêtes sont préparées via $wpdb->prepare() pour garantir la sécurité SQL.

Dans cette partie, vous avez appris comment suivre les visites d’articles et les visiteurs actifs tout en excluant les administrateurs et les robots. Votre plugin WordPress est désormais capable de collecter toutes les informations nécessaires pour générer des statistiques fiables.

Afficher les statistiques dans le tableau de bord WordPress

Un plugin WordPress est utile seulement si vous pouvez consulter facilement les données qu’il collecte. Pour notre plugin, il est essentiel de créer un tableau de bord clair et intuitif, permettant de visualiser :

  • Le nombre total de vues pour chaque article.
  • Le nombre de visiteurs actifs et la page qu’ils consultent.
  • Des filtres pour afficher les statistiques sur différentes périodes : depuis toujours, les 30 derniers jours, les 7 derniers jours ou hier.
  • Une pagination pour afficher les articles par lots de 50 et trier les plus lus en premier.

Nous allons créer une classe Stats_Admin qui s’occupera de cette interface, ainsi qu’un fichier CSS admin.css pour améliorer l’affichage.

Le fichier class-stats-admin.php

Ce fichier se trouve dans le dossier includes/ et contient la classe Stats_Admin. Voici le code complet :

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

class Stats_Admin {
    public static function init() {
        add_action( 'admin_menu', [ __CLASS__, 'add_menu' ] );
        add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue' ] );
    }

    public static function add_menu() {
        // top-level menu "Statistiques"
        add_menu_page(
            __( 'Statistiques', 'stats-visites' ),
            __( 'Statistiques', 'stats-visites' ),
            'manage_options',
            'stats-visites',
            [ __CLASS__, 'page_stats' ],
            'dashicons-chart-area',
            3
        );
    }

    public static function enqueue( $hook ) {
        if ( strpos( $hook, 'stats-visites' ) === false ) return;
        wp_enqueue_style( 'stats-visites-admin', STATS_VISITES_URL . 'assets/admin.css', [], '1.0' );
    }

    protected static function sanitize_range( $r ) {
        $allowed = [ 'all', '30', '7', '1' ];
        return in_array( $r, $allowed, true ) ? $r : 'all';
    }

    private function is_post_article($url) {
    // Reconstituer l'URL complète
    $full_url = home_url($url);

    // Récupérer l'ID WordPress de la ressource
    $post_id = url_to_postid($full_url);

    if ($post_id) {
        // Vérifier que c'est bien un article
        $post_type = get_post_type($post_id);
        return $post_type === 'post';
    }

    return false;
}

    public static function page_stats() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        global $wpdb;
        $prefix = $wpdb->prefix;

        // params
        $range = isset( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'all';
        $range = self::sanitize_range( $range );
        $paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1;
        $per_page = 50;
        $offset = ( $paged - 1 ) * $per_page;

        // active visitors: last_activity within 5 minutes
        $active_window = date( 'Y-m-d H:i:s', strtotime( '-5 minutes', current_time( 'timestamp' ) ) );
        $active_count = (int) $wpdb->get_var(
            $wpdb->prepare( "SELECT COUNT(*) FROM {$prefix}stats_sessions WHERE last_activity >= %s", $active_window )
        );
        $active_rows = $wpdb->get_results(
            $wpdb->prepare( "SELECT page, post_id, last_activity FROM {$prefix}stats_sessions WHERE last_activity >= %s ORDER BY last_activity DESC LIMIT 200", $active_window )
        );

        // build posts stats depending on range
        if ( $range === 'all' ) {
            // use totals table (fast)
            $sql_total = "SELECT t.post_id, t.view_count, p.post_title
                FROM {$prefix}stats_post_totals t
                LEFT JOIN {$wpdb->posts} p ON p.ID = t.post_id
                WHERE p.post_type = 'post'
                ORDER BY t.view_count DESC
                LIMIT %d OFFSET %d";
            $rows = $wpdb->get_results( $wpdb->prepare( $sql_total, $per_page, $offset ) );
            $total_items = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$prefix}stats_post_totals" );
        } else {
            // range in days: '30','7','1' (1 = yesterday)
            if ( $range === '1' ) {
                // yesterday full day
                $start = date( 'Y-m-d 00:00:00', strtotime( '-1 day', current_time( 'timestamp' ) ) );
                $end = date( 'Y-m-d 23:59:59', strtotime( '-1 day', current_time( 'timestamp' ) ) );
                $where = $wpdb->prepare( "WHERE sv.viewed_at BETWEEN %s AND %s", $start, $end );
            } else {
                $days = intval( $range );
                $start = date( 'Y-m-d H:i:s', strtotime( "-{$days} days", current_time( 'timestamp' ) ) );
                $where = $wpdb->prepare( "WHERE sv.viewed_at >= %s", $start );
            }

            $sql = "SELECT sv.post_id, COUNT(*) AS views, p.post_title
                FROM {$prefix}stats_post_views sv
                LEFT JOIN {$wpdb->posts} p ON p.ID = sv.post_id
                {$where}
                AND p.post_type = 'post'
                GROUP BY sv.post_id
                ORDER BY views DESC
                LIMIT %d OFFSET %d";
            $rows = $wpdb->get_results( $wpdb->prepare( $sql, $per_page, $offset ) );

            // total items for pagination
            $sql_count = "SELECT COUNT(DISTINCT sv.post_id) FROM {$prefix}stats_post_views sv
                LEFT JOIN {$wpdb->posts} p ON p.ID = sv.post_id
                {$where}
                AND p.post_type = 'post'";
            $total_items = (int) $wpdb->get_var( $sql_count );
        }

        // pagination math
        $total_pages = (int) ceil( $total_items / $per_page );

        // render (minimal)
        ?>
        <div class="wrap stats-visites-wrap">
            <h1><?php esc_html_e( 'Statistiques', 'stats-visites' ); ?></h1>

            <div class="stats-overview">
                <div class="stat-card">
                    <h2><?php echo intval( $active_count ); ?></h2>
                    <p><?php esc_html_e( 'Visiteurs actifs (5 min)', 'stats-visites' ); ?></p>
                </div>

                <div class="stat-card">
                    <form method="get" class="stats-filter-form">
                        <input type="hidden" name="page" value="stats-visites">
                        <label for="range"><?php esc_html_e( 'Période', 'stats-visites' ); ?></label>
                        <select id="range" name="range" onchange="this.form.submit()">
                            <option value="all" <?php selected( $range, 'all' ); ?>><?php esc_html_e( 'Depuis toujours', 'stats-visites' ); ?></option>
                            <option value="30" <?php selected( $range, '30' ); ?>><?php esc_html_e( '30 derniers jours', 'stats-visites' ); ?></option>
                            <option value="7" <?php selected( $range, '7' ); ?>><?php esc_html_e( '7 derniers jours', 'stats-visites' ); ?></option>
                            <option value="1" <?php selected( $range, '1' ); ?>><?php esc_html_e( 'Hier', 'stats-visites' ); ?></option>
                        </select>
                    </form>
                </div>
            </div>

            <div class="active-list">
                <h2><?php esc_html_e( 'Visiteurs actifs (aperçu)', 'stats-visites' ); ?></h2>
                <table class="widefat">
                    <thead><tr><th><?php esc_html_e( 'Page', 'stats-visites' ); ?></th><th><?php esc_html_e( 'Post ID', 'stats-visites' ); ?></th><th><?php esc_html_e( 'Dernière activité', 'stats-visites' ); ?></th></tr></thead>
                    <tbody>
                        <?php if ( $active_rows ): ?>
                            <?php foreach ( $active_rows as $r ): ?>
                                <?php
                                $post_id = $r->post_id;

                                // Si l'ID est vide, on tente de le déduire de l'URL
                                if ( ! $post_id ) {
                                    $post_id = url_to_postid( home_url( $r->page ) );
                                }

                                // Vérifier si c'est bien un article
                                if ( $post_id && get_post_type( $post_id ) === 'post' ):
                                ?>
                                    <tr>
                                        <td><?php echo esc_html( $r->page ); ?></td>
                                        <td><?php echo esc_html( $post_id ); ?></td>
                                        <td><?php echo esc_html( (new DateTime($r->last_activity))->format('H:i:s d/m/Y') ); ?></td>
                                    </tr>
                                <?php endif; ?>
                            <?php endforeach; ?>
                        <?php else: ?>
                            <tr>
                                <td colspan="3"><?php esc_html_e( 'Aucun visiteur actif', 'stats-visites' ); ?></td>
                            </tr>
                        <?php endif; ?>
                    </tbody>
                </table>
            </div>

            <div class="posts-list">
                <h2><?php esc_html_e( 'Articles - classement', 'stats-visites' ); ?></h2>
                <table class="widefat">
                    <thead><tr><th><?php esc_html_e( 'Titre', 'stats-visites' ); ?></th><th><?php esc_html_e( 'ID', 'stats-visites' ); ?></th><th><?php esc_html_e( 'Vues', 'stats-visites' ); ?></th></tr></thead>
                    <tbody>
                        <?php if ( $rows ): foreach ( $rows as $row ): ?>
                            <tr>
                                <td><?php echo esc_html( $row->post_title ?: '(no title)' ); ?></td>
                                <td><?php echo esc_html( $row->post_id ); ?></td>
                                <td><?php echo esc_html( isset( $row->view_count ) ? $row->view_count : ( isset( $row->views ) ? $row->views : 0 ) ); ?></td>
                            </tr>
                        <?php endforeach; else: ?>
                            <tr><td colspan="3"><?php esc_html_e( 'Aucun résultat', 'stats-visites' ); ?></td></tr>
                        <?php endif; ?>
                    </tbody>
                </table>

                <div class="pagination">
                    <?php
                    $base_url = esc_url_raw( add_query_arg( array( 'page' => 'stats-visites', 'range' => $range ), admin_url( 'admin.php' ) ) );
                    for ( $i = 1; $i <= max(1,$total_pages); $i++ ) {
                        $url = add_query_arg( 'paged', $i, $base_url );
                        $class = ( $i === $paged ) ? 'page-number current' : 'page-number';
                        echo '<a class="'.esc_attr($class).'" href="'.esc_url($url).'">'.intval($i).'</a> ';
                    }
                    ?>
                </div>
            </div>
        </div>
        <?php
    }
}

Ajout d’un menu dans le Back-Office

La méthode add_menu() utilise add_menu_page() pour ajouter un menu principal appelé « Statistiques » dans l’administration WordPress. Le menu est visible uniquement pour les utilisateurs ayant la capacité manage_options (les administrateurs). L’icône choisie est un graphique pour être visuellement reconnaissable.

Chargement du CSS

enqueue_styles() charge un fichier CSS spécifique uniquement lorsque nous sommes sur la page d’administration du plugin. Cela permet de ne pas alourdir inutilement l’interface d’administration ailleurs.

Affichage des statistiques

  • Filtre par période : l’utilisateur peut choisir de voir les statistiques depuis toujours, les 30 derniers jours, les 7 derniers jours ou seulement hier. La condition SQL est construite dynamiquement en fonction du filtre sélectionné.
  • Récupération des données : nous utilisons $wpdb->get_results() pour obtenir le nombre de vues par article depuis la table stats_post_views.
  • Tableau HTML : nous affichons les articles avec le nombre de vues dans un tableau WordPress standard, avec un lien vers l’édition de l’article.
  • Pagination : nous calculons le nombre total de pages et générons des liens pour naviguer entre les pages, affichant 50 articles par page.

Le fichier CSS admin.css

Dans le dossier assets/, créez un fichier admin.css pour améliorer l’affichage :

/* === Stats Visites - BackOffice === */

.stats-visites-wrap {
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    padding: 20px;
    background: #f9f9f9;
    color: #333;
}

.stats-visites-wrap h1 {
    font-size: 28px;
    margin-bottom: 20px;
    color: #0073aa;
}

.stats-overview {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    margin-bottom: 25px;
    justify-content: flex-start;
}

.stat-card {
    flex: 1 1 180px;
    background: #fff;
    border: 1px solid #e5e5e5;
    padding: 15px 20px;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.05);
    text-align: center;
    transition: transform 0.2s, box-shadow 0.2s;
}

.stat-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.stat-card h2 {
    font-size: 32px;
    margin: 0 0 8px;
    color: #0073aa;
}

.stat-card p {
    margin: 0;
    font-size: 14px;
    color: #555;
}

.stats-filter-form {
    margin: 0;
    display: flex;
    align-items: center;
    gap: 10px;
}

.stats-filter-form label {
    font-weight: 600;
}

.stats-filter-form select {
    padding: 5px 8px;
    border-radius: 5px;
    border: 1px solid #ccc;
    background: #fff;
    font-size: 14px;
    cursor: pointer;
    transition: border-color 0.2s;
}

.stats-filter-form select:hover {
    border-color: #0073aa;
}

.active-list, .posts-list {
    margin-top: 30px;
}

.active-list h2, .posts-list h2 {
    font-size: 20px;
    margin-bottom: 12px;
    color: #0073aa;
}

.widefat {
    width: 100%;
    border-collapse: collapse;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}

.widefat th {
    background: #f1f1f1;
    font-weight: 600;
    padding: 10px 12px;
    text-align: left;
    border-bottom: 2px solid #e5e5e5;
}

.widefat td {
    padding: 10px 12px;
    border-bottom: 1px solid #e5e5e5;
    vertical-align: middle;
}

.widefat tr:hover {
    background-color: #f5faff;
}

.pagination {
    margin-top: 15px;
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
}

.pagination .page-number {
    display: inline-block;
    padding: 7px 12px;
    border-radius: 6px;
    border: 1px solid #ddd;
    text-decoration: none;
    color: #333;
    transition: all 0.2s;
}

.pagination .page-number:hover {
    background: #0073aa;
    color: #fff;
    border-color: #0073aa;
}

.pagination .current {
    background: #0073aa;
    color: #fff;
    border-color: #0073aa;
    pointer-events: none;
}

@media (max-width: 768px) {
    .stats-overview {
        flex-direction: column;
    }
    .stat-card {
        width: 100%;
    }
    .stats-filter-form {
        flex-direction: column;
        align-items: flex-start;
    }
    .pagination {
        justify-content: flex-start;
    }
}

Ce CSS reste léger et améliore la lisibilité sans alourdir l’administration.

Installation, tests et conclusion du plugin WordPress

Une fois que vous avez créé tous les fichiers du plugin dans le dossier wp-content/plugins/stats-visites/, vous pouvez l’installer très simplement :

  1. Connectez-vous à votre administration WordPress.
  2. Dans le menu de gauche, allez sur « Extensions » puis « Ajouter ».
  3. Cliquez sur « Téléverser une extension » et sélectionnez le dossier compressé stats-visites.zip contenant tous vos fichiers, ou copiez directement le dossier complet stats-visites dans wp-content/plugins/.
  4. Activez le plugin.

Lors de l’activation, la méthode Stats_DB::activate() est appelée, et toutes les tables nécessaires sont créées automatiquement dans votre base de données.

Vérification du fonctionnement

Pour tester que le plugin fonctionne correctement :

  1. Vues des articles : Visitez vos articles côté front-end avec un navigateur différent de celui utilisé pour l’administration. Vous devriez voir le compteur de vues augmenter dans le tableau de bord.
  2. Exclusion des administrateurs : Connectez-vous en tant qu’administrateur et consultez vos articles. Ces visites ne doivent pas apparaître dans les statistiques.
  3. Exclusion des robots : Vérifiez que les visites provenant de bots (comme Googlebot) ne sont pas enregistrées.
  4. Visiteurs actifs : Dans le tableau de bord, vous pouvez voir quels visiteurs sont actuellement actifs et sur quelle page ils se trouvent.
  5. Filtres et pagination : Testez les filtres temporels (Depuis toujours, 30 derniers jours, 7 derniers jours, Hier) et naviguez entre les pages pour vous assurer que la pagination fonctionne correctement.

Bonnes pratiques pour maintenir votre plugin

  1. Sécurité : Vérifiez régulièrement que les données sont échappées correctement (sanitize_text_fieldesc_url_raw, etc.) pour éviter les injections SQL ou les problèmes XSS.
  2. Performance : La table stats_post_totals permet un affichage rapide des articles les plus vus. Vous pouvez aussi mettre en place un nettoyage périodique des tables stats_post_views et stats_sessions pour ne pas surcharger la base de données.
  3. Compatibilité : Testez le plugin sur différentes versions de WordPress et avec vos autres extensions pour éviter les conflits.
  4. Améliorations futures : Vous pourriez ajouter des graphiques pour visualiser les visites, un export CSV des statistiques ou encore un système de notifications lorsque certains articles atteignent un nombre de vues précis.

Vous avez désormais toutes les clés en main pour créer un plugin WordPress complet et fonctionnel capable de compter les vues de vos articles et de suivre les visiteurs actifs. Ce plugin respecte la vie privée des utilisateurs en n’enregistrant pas les administrateurs et en filtrant les robots. Il offre une interface claire avec filtres et pagination, et reste léger pour ne pas alourdir votre site.

En suivant ce tutoriel, vous avez appris à :

  • Coder un plugin WordPress depuis zéro.
  • Concevoir une base de données efficace pour le suivi des vues.
  • Implémenter un suivi précis des visiteurs tout en excluant les robots et administrateurs.
  • Construire une interface d’administration conviviale pour consulter les statistiques.
  • Optimiser le code pour la performance et la sécurité.

Au-delà du simple comptage de vues, ce plugin vous ouvre la voie vers de nombreuses améliorations possibles, comme l’analyse de tendances, la génération de rapports ou l’intégration avec d’autres outils WordPress. Vous avez maintenant les compétences pour adapter et personnaliser ce plugin selon vos besoins spécifiques, et même créer d’autres plugins optimisés pour votre blog.

Créer votre propre plugin WordPress n’est pas seulement une solution pratique pour gérer vos statistiques, c’est également un excellent exercice pour approfondir votre compréhension de WordPress, PHP et MySQL, tout en gardant un contrôle total sur vos données et votre site.

Téléchargez ce Plugin WordPress pour les stats de vues des articles.