Une agence maintient une extension d’affichage d’avis clients, distribuée à environ deux cents sites construits avec des dizaines de thèmes différents, dont une bonne moitié encore sur des bases classiques héritées de 2016 ou 2017, sans le moindre bloc Gutenberg dans leurs zones de widgets. La question posée en réunion d’équipe est simple : faut-il encore, en 2020, écrire un nouveau widget avec register_widget(), ou est-ce du temps perdu sur une API qu’on annonce mourante depuis l’arrivée de l’éditeur de blocs ?
La réponse ne tient pas à une mode technique, mais au parc réel que l’extension doit servir. Et pour une extension multi-thèmes destinée à durer, ignorer les widgets classiques revient à couper l’accès à une bonne partie de sa base installée.
Ce que Gutenberg a changé, et ce qu’il n’a pas changé
Depuis WordPress 5.0, l’éditeur de blocs a remplacé l’éditeur de contenu classique, et un widget de type bloc peut désormais être inséré dans un article ou une page comme n’importe quel autre bloc. Mais les zones de widgets de la barre latérale ou du pied de page, elles, continuent d’utiliser l’écran widgets.php classique, avec son système de glisser-déposer hérité. Rien dans le cœur de WordPress, à la date de cet article, ne transforme automatiquement ces zones en zones à blocs : ce chantier existe dans les discussions de la feuille de route de Gutenberg, mais aucune version publiée ne l’implémente encore.
Concrètement, un thème classique appelle toujours dynamic_sidebar() dans son fichier sidebar.php, et cette fonction continue de fonctionner exactement comme avant, en listant les widgets enregistrés via register_widget() et en les rendant dans l’ordre choisi par l’administrateur du site.
Le cas où le widget classique reste justifié

Pour une extension qui vise un public de thèmes variés, souvent installés depuis des années sur des sites de PME ou d’associations, register_widget reste le seul mécanisme garanti de fonctionner partout, sans supposer que le thème a été mis à jour ou reconstruit avec les blocs en tête. Voici un squelette minimal, conforme à l’API :
class Widget_Avis_Clients extends WP_Widget {
public function __construct() {
parent::__construct(
'avis_clients',
__( 'Avis clients', 'avis-clients' ),
array(
'classname' => 'widget-avis-clients',
'description' => __( 'Affiche les derniers avis validés.', 'avis-clients' ),
'customize_selective_refresh' => true,
)
);
}
public function widget( $args, $instance ) {
$titre = isset( $instance['titre'] ) ? $instance['titre'] : '';
$nombre = isset( $instance['nombre'] ) ? absint( $instance['nombre'] ) : 3;
echo $args['before_widget'];
if ( $titre ) {
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $titre, $instance, $this->id_base ) ) . $args['after_title'];
}
$avis = get_posts( array(
'post_type' => 'avis',
'posts_per_page' => $nombre,
'no_found_rows' => true,
) );
foreach ( $avis as $un_avis ) {
echo '<p class="avis">' . esc_html( get_the_title( $un_avis ) ) . '</p>';
}
echo $args['after_widget'];
}
public function form( $instance ) {
$titre = isset( $instance['titre'] ) ? $instance['titre'] : '';
$nombre = isset( $instance['nombre'] ) ? absint( $instance['nombre'] ) : 3;
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'titre' ) ); ?>"><?php esc_html_e( 'Titre', 'avis-clients' ); ?></label>
<input class="widefat" type="text"
id="<?php echo esc_attr( $this->get_field_id( 'titre' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'titre' ) ); ?>"
value="<?php echo esc_attr( $titre ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'nombre' ) ); ?>"><?php esc_html_e( 'Nombre d’avis', 'avis-clients' ); ?></label>
<input class="tiny-text" type="number" min="1" max="10" step="1"
id="<?php echo esc_attr( $this->get_field_id( 'nombre' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'nombre' ) ); ?>"
value="<?php echo esc_attr( $nombre ); ?>">
</p>
<?php
}
public function update( $nouvelle_instance, $ancienne_instance ) {
return array(
'titre' => sanitize_text_field( $nouvelle_instance['titre'] ),
'nombre' => min( 10, max( 1, absint( $nouvelle_instance['nombre'] ) ) ),
);
}
}
add_action( 'widgets_init', function () {
register_widget( 'Widget_Avis_Clients' );
} );
Ce squelette contient tout ce que WordPress attend d’un widget. Le constructeur du parent reçoit l’identifiant de base, le nom affiché dans l’administration et un tableau d’options. La méthode widget() produit le rendu public, form() dessine le formulaire de l’administration, et update() nettoie les valeurs avant leur enregistrement. L’enregistrement lui-même se fait sur widgets_init, jamais plus tôt : à ce moment, le gestionnaire de widgets existe et les zones déclarées par les thèmes sont connues.
Les quatre méthodes, une par une
Le constructeur n’a qu’une tâche, mais elle conditionne tout le reste. L’identifiant de base, ici avis_clients, sert de préfixe aux réglages stockés dans la table des options : WordPress conserve les instances d’un même widget dans une seule option, sous la forme widget_avis_clients. Changer cet identifiant après la mise en ligne fait perdre tous les réglages déjà saisis par les administrateurs, sans message d’erreur. Choisissez-le une fois pour toutes, et préfixez-le avec le nom de l’extension si vous craignez une collision.
La méthode widget() reçoit deux tableaux. Le premier, $args, provient de la zone de widgets : c’est le thème qui décide des balises before_widget, after_widget, before_title et after_title. Un widget qui ignore ces quatre clés s’affiche de travers dans la moitié des thèmes, puisque chacun attend son propre balisage. Le second, $instance, contient les réglages enregistrés pour cette instance précise, et peut être vide si le widget vient d’être ajouté : testez toujours l’existence de chaque clé.
La méthode form() s’appuie sur get_field_id() et get_field_name(). Ces deux fonctions fabriquent des attributs uniques par instance, ce qui permet de placer plusieurs fois le même widget dans la même zone sans que les champs se mélangent. Écrire l’attribut name à la main est l’erreur classique : le formulaire s’affiche, semble fonctionner, mais les valeurs ne sont jamais enregistrées.
Enfin, update() est le seul endroit où l’entrée de l’administrateur devient une donnée stockée. C’est là que l’on nettoie et que l’on borne : un nombre d’avis compris entre 1 et 10, un titre réduit à du texte brut. La protection à l’affichage, avec esc_html(), reste indispensable même si update() a déjà nettoyé, car une donnée peut arriver dans la base par une autre voie.
Le cas concret : l’extension d’avis clients
Reprenons la situation de l’agence. L’extension affiche déjà les avis dans une page dédiée, grâce à un type de contenu propre. Les thèmes classiques qui la reçoivent proposent presque tous une barre latérale : un widget est donc le moyen le plus direct de montrer les trois derniers avis sur toutes les pages, sans demander au client de modifier un seul fichier du thème.
Deux décisions rendent ce widget durable. La première consiste à ne pas dupliquer la logique de récupération des avis : la boucle vit dans une fonction de l’extension, que le widget, un futur bloc et un shortcode appellent tous. La seconde consiste à limiter le coût : no_found_rows évite le calcul d’une pagination inutile, et le nombre d’avis reste borné par update(). Pour un widget affiché sur chaque page du site, ces deux précautions comptent davantage que l’élégance du code.
Un widget n’est pas une fonctionnalité, c’est une vitrine : il doit rester mince, et laisser la logique vivre ailleurs, là où plusieurs interfaces pourront la réutiliser.
Les pièges à éviter
- Changer l’identifiant de base ou le nom de la classe après une mise en production : les réglages enregistrés deviennent orphelins.
- Oublier
customize_selective_refresh: le widget fonctionne, mais l’aperçu du personnalisateur se recharge entièrement à chaque frappe, ce qui donne une impression de lenteur. - Faire des requêtes lourdes dans
widget()sans mise en cache : le code s’exécute à chaque affichage de chaque page où la zone apparaît. - Afficher du contenu sans échappement : le titre saisi dans l’administration est une donnée, pas du code de confiance.
- Enregistrer le widget trop tôt : en dehors de
widgets_init, l’appel àregister_widget()peut échouer silencieusement.
Quand ne pas écrire un widget classique
Si votre extension s’adresse à un parc homogène de sites récents, construits avec des thèmes qui gèrent déjà les blocs dans leurs zones, l’investissement se déplace : un bloc dynamique, déclaré côté serveur avec une fonction de rendu, vous servira dans les articles, les pages et, selon l’évolution de l’éditeur, dans les zones de widgets. À la date de cet article, les zones de widgets par blocs ne sont encore qu’une expérimentation du plugin Gutenberg : s’y fier pour une extension distribuée à grande échelle serait prématuré.
Entre les deux, une stratégie prudente consiste à séparer le code : une fonction de rendu unique, un widget classique mince qui l’appelle, et un shortcode qui l’appelle aussi. Le jour où les zones de widgets accepteront les blocs en standard, vous n’aurez qu’à ajouter une couche, sans réécrire la logique.
Conclusion
Écrire un widget classique en 2020 n’a rien d’archaïque, à condition de le faire pour les bonnes raisons : un parc de thèmes varié, des sites que personne ne reconstruira de sitôt, une API stable depuis des années. Le critère n’est pas la mode de l’éditeur, mais la réalité des installations que vous devez servir. Gardez le widget mince, nettoyez dans update(), échappez dans widget(), et préparez calmement la suite en isolant votre logique.