Avec 60 000+ plugins dans le répertoire officiel, créer un plugin WordPress qui se démarque demande plus qu’un simple add_action(). Dans ce tuto, on va construire un plugin complet et professionnel en suivant les standards 2026 : PHP 8.3, autoloading, REST API, page de réglages et sécurité.
Notre plugin s’appellera WP Reading Time : il ajoutera un temps de lecture estimé en haut de chaque article, avec une page de réglages pour personnaliser l’affichage.
1. Structure du plugin
wp-reading-time/
├── wp-reading-time.php # Fichier principal
├── composer.json # Autoloading PSR-4
├── readme.txt # Répertoire WordPress.org
├── src/
│ ├── Plugin.php # Classe principale
│ ├── ReadingTime.php # Logique métier
│ ├── Settings.php # Page de réglages admin
│ └── REST.php # Endpoint API REST
├── assets/
│ ├── css/
│ │ └── reading-time.css
│ └── js/
│ └── reading-time.js
└── languages/
└── wp-reading-time.pot
2. Le fichier principal (wp-reading-time.php)
<?php
/**
* Plugin Name: WP Reading Time
* Description: Ajoute un temps de lecture estimé aux articles
* Version: 1.0.0
* Author: WP Admin Lab
* License: GPL-2.0+
* Text Domain: wp-reading-time
* Requires PHP: 8.1
* Requires WP: 6.5
*/
declare(strict_types=1);
// Sécurité : empêcher l'accès direct
if (!defined('ABSPATH')) {
exit;
}
// Autoloader Composer (si présent)
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
// Constantes du plugin
define('WPRT_VERSION', '1.0.0');
define('WPRT_PATH', plugin_dir_path(__FILE__));
define('WPRT_URL', plugin_dir_url(__FILE__));
// Initialisation
require_once __DIR__ . '/src/Plugin.php';
require_once __DIR__ . '/src/ReadingTime.php';
require_once __DIR__ . '/src/Settings.php';
require_once __DIR__ . '/src/REST.php';
// Bootstrap
add_action('init', function () {
\WPReadingTime\Plugin::init();
});
3. La classe principale (Plugin.php)
<?php
namespace WPReadingTime;
class Plugin
{
public static function init(): void
{
// Hooks
add_filter('the_content', [ReadingTime::class, 'addReadingTime']);
add_action('wp_enqueue_scripts', [self::class, 'enqueueAssets']);
add_action('admin_menu', [Settings::class, 'register']);
add_action('rest_api_init', [REST::class, 'register']);
// Activation / Désactivation
register_activation_hook(WPRT_PATH . 'wp-reading-time.php', [self::class, 'activate']);
register_deactivation_hook(WPRT_PATH . 'wp-reading-time.php', [self::class, 'deactivate']);
}
public static function enqueueAssets(): void
{
if (is_single()) {
wp_enqueue_style(
'wp-reading-time',
WPRT_URL . 'assets/css/reading-time.css',
[],
WPRT_VERSION
);
}
}
public static function activate(): void
{
add_option('wprt_words_per_minute', 200);
add_option('wprt_display_position', 'top');
add_option('wprt_text_template', '⏱️ %d min de lecture');
}
public static function deactivate(): void
{
// Nettoyage propre si nécessaire
}
}
4. La logique métier (ReadingTime.php)
<?php
namespace WPReadingTime;
class ReadingTime
{
public static function addReadingTime(string $content): string
{
if (!is_single() || !in_the_loop() || !is_main_query()) {
return $content;
}
$word_count = str_word_count(wp_strip_all_tags($content));
$wpm = (int) get_option('wprt_words_per_minute', 200);
$minutes = (int) ceil($word_count / $wpm);
if ($minutes < 1) {
return $content;
}
$template = get_option('wprt_text_template', '⏱️ %d min de lecture');
$reading_time = sprintf($template, $minutes);
$html = sprintf(
'<div class="wp-reading-time" aria-label="Temps de lecture estimé">%s</div>',
esc_html($reading_time)
);
$position = get_option('wprt_display_position', 'top');
return $position === 'top'
? $html . $content
: $content . $html;
}
}
5. Page de réglages (Settings.php)
<?php
namespace WPReadingTime;
class Settings
{
public static function register(): void
{
add_options_page(
'WP Reading Time',
'Reading Time',
'manage_options',
'wp-reading-time',
[self::class, 'render']
);
add_action('admin_init', [self::class, 'registerSettings']);
}
public static function registerSettings(): void
{
register_setting('wprt_settings', 'wprt_words_per_minute', [
'type' => 'integer',
'default' => 200,
'sanitize_callback' => 'absint',
]);
register_setting('wprt_settings', 'wprt_display_position', [
'type' => 'string',
'default' => 'top',
'sanitize_callback' => fn($v) => in_array($v, ['top', 'bottom']) ? $v : 'top',
]);
register_setting('wprt_settings', 'wprt_text_template', [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
]);
}
public static function render(): void
{
?>
<div class="wrap">
<h1><?= esc_html(get_admin_page_title()) ?></h1>
<form method="post" action="options.php">
<?php
settings_fields('wprt_settings');
do_settings_sections('wprt_settings');
?>
<table class="form-table">
<tr><th>Mots par minute</th><td><input type="number" name="wprt_words_per_minute" value="<?= esc_attr(get_option('wprt_words_per_minute', 200)) ?>"></td></tr>
<tr><th>Position</th><td><select name="wprt_display_position"><option value="top" <?= selected(get_option('wprt_display_position'), 'top', false) ?>>Avant le contenu</option><option value="bottom" <?= selected(get_option('wprt_display_position'), 'bottom', false) ?>>Après le contenu</option></select></td></tr>
<tr><th>Template</th><td><input type="text" name="wprt_text_template" value="<?= esc_attr(get_option('wprt_text_template')) ?>" class="regular-text"></td></tr>
</table>
<?php submit_button() ?>
</form>
</div>
<?php
}
}
6. Endpoint REST API (REST.php)
En 2026, tout plugin sérieux expose une API REST. Notre endpoint renverra le temps de lecture d’un article via l’API WordPress.
<?php
namespace WPReadingTime;
class REST
{
public static function register(): void
{
register_rest_route('wp-reading-time/v1', '/post/(?P<id>\d+)', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [self::class, 'getReadingTime'],
'args' => [
'id' => [
'validate_callback' => fn($v) => is_numeric($v),
'required' => true,
],
],
'permission_callback' => '__return_true',
]);
}
public static function getReadingTime(\WP_REST_Request $request): \WP_REST_Response
{
$post_id = (int) $request->get_param('id');
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
return new \WP_REST_Response(
['error' => 'Article introuvable'],
404
);
}
$word_count = str_word_count(wp_strip_all_tags($post->post_content));
$wpm = (int) get_option('wprt_words_per_minute', 200);
$minutes = (int) ceil($word_count / $wpm);
return new \WP_REST_Response([
'post_id' => $post_id,
'post_title' => $post->post_title,
'word_count' => $word_count,
'reading_time' => $minutes,
'unit' => $minutes > 1 ? 'minutes' : 'minute',
], 200);
}
}
// Test : GET /wp-json/wp-reading-time/v1/post/42
Cette API permet à n’importe quel front-end (React, Vue, mobile) de récupérer le temps de lecture sans passer par le rendu PHP.
7. Internationalisation : prêt pour la traduction
// Dans Plugin.php, remplacer les strings en dur :
__('⏱️ %d min de lecture', 'wp-reading-time');
// Générer le fichier .pot avec WP-CLI :
// wp i18n make-pot wp-reading-time/ wp-reading-time/languages/wp-reading-time.pot
Un plugin sans internationalisation est invisible pour 70% des utilisateurs WordPress dans le monde. C’est 30 minutes de travail pour un reach global.
8. Les bonnes pratiques 2026
Ce qui fait la diff entre un plugin amateur et un plugin pro :
- PHP 8.3 strict —
declare(strict_types=1)sur tous les fichiers - Namespaces PSR-4 — plus de fonctions globales qui entrent en conflit
- Escaping systématique —
esc_html(),esc_attr(),wp_kses() - Hooks bien nommés — noms en
snake_caseoukebab-case, pas de mélange - Pas de jQuery — vanilla JS ou un micro-framework de 2 KB
- Compatible Playground — tester avec SQLite, pas seulement MySQL
- README.txt complet — c’est votre page de vente sur WordPress.org
7. Installation et test
# Créer le zip
cd wp-reading-time && zip -r ../wp-reading-time.zip . -x "*.git*" "*.DS_Store"
# Tester avec WP-CLI
wp plugin install ./wp-reading-time.zip --activate
wp option get wprt_words_per_minute
# Vérifier le temps de lecture
wp post get 1 --field=content | wc -w
Ce plugin est prêt à être soumis sur WordPress.org. Il est sécurisé, internationalisable, et compatible PHP 8.1+. Adaptez le namespace et le nom, et vous avez votre premier plugin pro en 2026.
Commentaires (0)
Laisser un commentaire
Les commentaires sont modérés. Questions WordPress, cybersécurité ou dev web bienvenues.