Si votre client vous demande “un bloc Elementor qui affiche un encart auteur avec un lien vers ses derniers articles”, vous avez deux choix : bricoler un shortcode, ou créer un vrai widget Elementor, proprement configurable et maintenable. Le shortcode marche… jusqu’au jour où vous devez ajouter un contrôle de style, gérer un fallback, ou éviter de charger du CSS partout.
Le problème / Le besoin
Vous voulez un widget Elementor personnalisé, réutilisable, qui s’insère dans l’éditeur comme n’importe quel widget natif. Et surtout : vous voulez qu’il soit configurable (contenu + styles), sécurisé (sanitization/escaping), et qu’il n’impacte pas les performances du site.
Ce guide s’adresse à des utilisateurs intermédiaires (à l’aise avec PHP et les hooks) qui travaillent sur WordPress 6.9.4 (avril 2026) et PHP 8.1+. À la fin, vous saurez :
- Créer un mini-plugin qui enregistre un widget via l’API Widget d’Elementor.
- Ajouter des contrôles (texte, URL, sélecteur d’utilisateur, nombre d’articles, bascule d’options).
- Générer un rendu HTML sûr (escaping) et robuste (fallbacks).
- Charger CSS/JS uniquement quand le widget est utilisé.
Résumé rapide
- On crée un plugin “mu” ou classique qui s’accroche à
elementor/widgets/register. - On définit une classe de widget qui étend
ElementorWidget_Base. - On ajoute des contrôles via
Controls_Manager(contenu + style). - On rend le widget avec
render()(front) etcontent_template()(aperçu éditeur, optionnel). - On enregistre des assets (CSS/JS) et on les charge via
get_style_depends()/get_script_depends().
Quand utiliser cette solution
- Vous avez un bloc spécifique au projet (ex : “Encart auteur”, “CTA maison”, “Produits mis en avant”) que vos éditeurs doivent pouvoir configurer visuellement.
- Vous devez exposer des contrôles de style Elementor (typographie, couleurs, espacements) sans écrire une usine à gaz de classes CSS.
- Vous voulez éviter les shortcodes “magiques” qui cassent la mise en page quand on change de thème ou de builder.
- Vous maintenez plusieurs sites : un widget packagé en plugin est plus simple à versionner et déployer.
Quand ne PAS utiliser cette solution
- Vous voulez juste insérer un petit bout de HTML statique : utilisez un widget “HTML” ou un modèle Elementor.
- Vous avez besoin d’un rendu 100% dynamique côté serveur mais sans UI complexe : un shortcode peut suffire (et être consommé dans Elementor via widget “Shortcode”).
- Vous cherchez un composant réutilisable cross-builders : privilégiez un bloc Gutenberg (Block Editor) et/ou un pattern. Elementor est un écosystème spécifique.
- Vous n’avez pas la main sur le code (site client verrouillé) : un plugin “snippets” peut dépanner, mais c’est rarement propre pour des classes/chargements d’assets.
Prérequis / avant de commencer
Avant de toucher au code :
- Travaillez sur un environnement de staging/local (LocalWP, DevKinsta, Docker…).
- Sauvegardez la base et les fichiers (au minimum
wp-content). - Vérifiez les versions : WordPress 6.9.4, PHP 8.1+, Elementor à jour.
- Activez
WP_DEBUGetWP_DEBUG_LOGsur staging pour voir les erreurs.
Rappels utiles :
- Référence WP Plugin Handbook : developer.wordpress.org/plugins
- Bonnes pratiques de sécurité (validation/escaping) : developer.wordpress.org/apis/security
- PHP 8.1 : php.net/releases/8.1
Précaution sécurité : un widget Elementor peut afficher des données issues de la base (utilisateurs, posts). Si vous échappez mal les sorties, vous ouvrez la porte à des XSS stockées. J’ai déjà vu ce scénario sur des sites multi-auteurs où un “display_name” mal filtré finissait injecté dans un attribut HTML.
L’approche naïve (et pourquoi l’éviter)
Ce que je vois souvent : un shortcode qui récupère des options via $_GET ou des attributs non filtrés, puis imprime du HTML brut. Exemple typique (à ne pas utiliser) :
<?php
// ❌ Exemple volontairement mauvais : pas de sanitization, pas d'escaping, requête non bornée.
add_shortcode('author_box', function($atts) {
$atts = shortcode_atts([
'user' => 1,
'count' => 5,
'title' => 'Auteur'
], $atts);
$user = get_user_by('id', $atts['user']);
echo '<div class="author-box">';
echo '<h3>' . $atts['title'] . '</h3>';
echo '<p>' . $user->display_name . '</p>';
echo '</div>';
});
Problèmes concrets :
- Sécurité :
$atts['title']et$user->display_namesortent sansesc_html(). - Perf : pas de cache, pas de bornes strictes, et vous risquez de charger des requêtes répétées dans une page Elementor.
- UX : dans Elementor, l’éditeur ne voit pas des contrôles natifs (pas de typographie/couleurs sans CSS custom).
- Maintenance : vous finissez avec 12 shortcodes, chacun avec sa logique et son CSS global.
La bonne approche — tutoriel pas à pas
Étape 1 — Créer un plugin minimal
Créez un dossier : wp-content/plugins/bpcab-elementor-widgets
Créez le fichier principal : wp-content/plugins/bpcab-elementor-widgets/bpcab-elementor-widgets.php
Étape 2 — Vérifier qu’Elementor est chargé (et au bon moment)
Le piège classique : enregistrer le widget trop tôt (ex : sur init) et obtenir une erreur du type Class ‘ElementorWidget_Base’ not found. On s’accroche aux hooks Elementor dédiés.
Étape 3 — Déclarer un widget “Encart Auteur + derniers posts”
On va coder un widget qui :
- Choisit un utilisateur (auteur) via un contrôle.
- Affiche son avatar + nom + bio (optionnel).
- Liste ses derniers articles (nombre configurable).
- Expose des options de style (couleurs, typographie, espacement).
Étape 4 — Charger CSS/JS seulement si nécessaire
Elementor permet de déclarer des dépendances via get_style_depends() et get_script_depends(). Dans mon expérience, c’est un gros gain : vous évitez un fichier CSS global chargé sur toutes les pages.
Étape 5 — Ajouter des contrôles de style Elementor
On utilisera les “selectors” d’Elementor pour générer du CSS scoped au widget. Ça évite de coder des dizaines de classes et ça réduit les conflits avec Avada/Divi.
Étape 6 — Rendu sécurisé et robuste
Points clés :
- Sanitization côté réglages :
absint,sanitize_text_field,esc_url_rawsi besoin. - Escaping côté sortie :
esc_html,esc_attr,esc_url. - Fallbacks : auteur introuvable, pas d’articles, bio vide.
Code complet
Copiez-collez tel quel. Ce plugin enregistre 1 widget Elementor. Il est volontairement compact mais complet et prêt à tester.
1) Fichier principal du plugin
<?php
/**
* Plugin Name: BPCAB - Widgets Elementor (Exemple)
* Description: Exemple pédagogique : widget Elementor personnalisé (encart auteur + derniers articles).
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
* Author: BPCAB
*
* Sécurité : ce plugin est un exemple. Testez en staging avant production.
*/
declare(strict_types=1);
if (!defined('ABSPATH')) {
exit;
}
final class BPCAB_Elementor_Widgets_Plugin {
private const MIN_PHP = '8.1';
public static function init(): void {
add_action('plugins_loaded', [__CLASS__, 'bootstrap']);
}
public static function bootstrap(): void {
// Vérif PHP (utile si le site est downgradé par erreur).
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
add_action('admin_notices', [__CLASS__, 'notice_php_version']);
return;
}
// Ne rien faire si Elementor n'est pas actif.
if (!did_action('elementor/loaded')) {
add_action('admin_notices', [__CLASS__, 'notice_elementor_missing']);
return;
}
// Enregistrer le widget au bon hook.
add_action('elementor/widgets/register', [__CLASS__, 'register_widgets']);
// Enregistrer les assets (CSS/JS) utilisables par les widgets.
add_action('wp_enqueue_scripts', [__CLASS__, 'register_front_assets']);
}
public static function register_front_assets(): void {
$ver = '1.0.0';
wp_register_style(
'bpcab-author-box',
plugins_url('assets/author-box.css', __FILE__),
[],
$ver
);
// JS optionnel : ici on ne fait rien de critique, mais c'est prêt si vous en avez besoin.
wp_register_script(
'bpcab-author-box',
plugins_url('assets/author-box.js', __FILE__),
[],
$ver,
true
);
}
public static function register_widgets($widgets_manager): void {
// Charger la classe du widget.
require_once __DIR__ . '/widgets/class-bpcab-author-box-widget.php';
// Elementor 3.x+ : register() existe sur le manager.
$widgets_manager->register(new BPCAB_Author_Box_Widget());
}
public static function notice_elementor_missing(): void {
if (!current_user_can('activate_plugins')) {
return;
}
$plugin_page = admin_url('plugins.php');
echo '<div class="notice notice-warning"><p>';
echo esc_html__('BPCAB - Widgets Elementor : Elementor n’est pas actif. Activez Elementor pour charger le widget.', 'bpcab');
echo ' ';
echo '<a href="' . esc_url($plugin_page) . '">' . esc_html__('Aller aux extensions', 'bpcab') . '</a>';
echo '</p></div>';
}
public static function notice_php_version(): void {
if (!current_user_can('manage_options')) {
return;
}
echo '<div class="notice notice-error"><p>';
echo esc_html__('BPCAB - Widgets Elementor : PHP 8.1+ est requis.', 'bpcab');
echo '</p></div>';
}
}
BPCAB_Elementor_Widgets_Plugin::init();
2) Classe du widget
Créez : wp-content/plugins/bpcab-elementor-widgets/widgets/class-bpcab-author-box-widget.php
<?php
declare(strict_types=1);
if (!defined('ABSPATH')) {
exit;
}
use ElementorWidget_Base;
use ElementorControls_Manager;
use ElementorGroup_Control_Typography;
use ElementorGroup_Control_Border;
use ElementorGroup_Control_Box_Shadow;
final class BPCAB_Author_Box_Widget extends Widget_Base {
public function get_name(): string {
return 'bpcab_author_box';
}
public function get_title(): string {
return esc_html__('Encart Auteur (BPCAB)', 'bpcab');
}
public function get_icon(): string {
// Icône Elementor (dashicons-like). Vous pouvez la changer.
return 'eicon-user-circle-o';
}
public function get_categories(): array {
// Catégorie standard. Vous pouvez créer votre propre catégorie si besoin.
return ['general'];
}
public function get_keywords(): array {
return ['author', 'auteur', 'bio', 'posts', 'bpcab'];
}
public function get_style_depends(): array {
return ['bpcab-author-box'];
}
public function get_script_depends(): array {
return ['bpcab-author-box'];
}
protected function register_controls(): void {
// SECTION : Contenu
$this->start_controls_section(
'section_content',
[
'label' => esc_html__('Contenu', 'bpcab'),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
$this->add_control(
'title',
[
'label' => esc_html__('Titre', 'bpcab'),
'type' => Controls_Manager::TEXT,
'default' => esc_html__('À propos de l’auteur', 'bpcab'),
'placeholder' => esc_html__('Ex : À propos de Marie', 'bpcab'),
'label_block' => true,
]
);
// Contrôle simple : ID utilisateur (numérique).
// Variante plus avancée : select2 alimenté en AJAX (hors scope), ou select statique.
$this->add_control(
'user_id',
[
'label' => esc_html__('ID utilisateur (auteur)', 'bpcab'),
'type' => Controls_Manager::NUMBER,
'min' => 1,
'step' => 1,
'default' => (int) get_current_user_id(),
'description' => esc_html__('Astuce : récupérez l’ID dans Utilisateurs > Tous les utilisateurs.', 'bpcab'),
]
);
$this->add_control(
'show_bio',
[
'label' => esc_html__('Afficher la bio', 'bpcab'),
'type' => Controls_Manager::SWITCHER,
'label_on' => esc_html__('Oui', 'bpcab'),
'label_off' => esc_html__('Non', 'bpcab'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->add_control(
'show_posts',
[
'label' => esc_html__('Afficher les derniers articles', 'bpcab'),
'type' => Controls_Manager::SWITCHER,
'label_on' => esc_html__('Oui', 'bpcab'),
'label_off' => esc_html__('Non', 'bpcab'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->add_control(
'posts_count',
[
'label' => esc_html__('Nombre d’articles', 'bpcab'),
'type' => Controls_Manager::NUMBER,
'min' => 1,
'max' => 12,
'step' => 1,
'default' => 3,
'condition' => [
'show_posts' => 'yes',
],
]
);
$this->add_control(
'profile_url',
[
'label' => esc_html__('URL du profil (optionnel)', 'bpcab'),
'type' => Controls_Manager::URL,
'placeholder' => 'https://',
'show_external' => true,
'description' => esc_html__('Si vide, le nom n’est pas cliquable.', 'bpcab'),
]
);
$this->end_controls_section();
// SECTION : Style (encart)
$this->start_controls_section(
'section_style_box',
[
'label' => esc_html__('Style : Encart', 'bpcab'),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$this->add_control(
'box_bg',
[
'label' => esc_html__('Couleur de fond', 'bpcab'),
'type' => Controls_Manager::COLOR,
'default' => '',
'selectors' => [
'{{WRAPPER}} .bpcab-author-box' => 'background-color: {{VALUE}};',
],
]
);
$this->add_responsive_control(
'box_padding',
[
'label' => esc_html__('Padding', 'bpcab'),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => ['px', 'em', 'rem', '%'],
'selectors' => [
'{{WRAPPER}} .bpcab-author-box' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
]
);
$this->add_group_control(
Group_Control_Border::get_type(),
[
'name' => 'box_border',
'selector' => '{{WRAPPER}} .bpcab-author-box',
]
);
$this->add_group_control(
Group_Control_Box_Shadow::get_type(),
[
'name' => 'box_shadow',
'selector' => '{{WRAPPER}} .bpcab-author-box',
]
);
$this->end_controls_section();
// SECTION : Style (typos)
$this->start_controls_section(
'section_style_text',
[
'label' => esc_html__('Style : Texte', 'bpcab'),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$this->add_control(
'title_color',
[
'label' => esc_html__('Couleur du titre', 'bpcab'),
'type' => Controls_Manager::COLOR,
'selectors' => [
'{{WRAPPER}} .bpcab-author-box__title' => 'color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'name' => 'title_typography',
'selector' => '{{WRAPPER}} .bpcab-author-box__title',
]
);
$this->add_control(
'name_color',
[
'label' => esc_html__('Couleur du nom', 'bpcab'),
'type' => Controls_Manager::COLOR,
'selectors' => [
'{{WRAPPER}} .bpcab-author-box__name' => 'color: {{VALUE}};',
],
]
);
$this->add_group_control(
Group_Control_Typography::get_type(),
[
'name' => 'name_typography',
'selector' => '{{WRAPPER}} .bpcab-author-box__name',
]
);
$this->add_control(
'bio_color',
[
'label' => esc_html__('Couleur de la bio', 'bpcab'),
'type' => Controls_Manager::COLOR,
'selectors' => [
'{{WRAPPER}} .bpcab-author-box__bio' => 'color: {{VALUE}};',
],
'condition' => [
'show_bio' => 'yes',
],
]
);
$this->end_controls_section();
}
protected function render(): void {
$settings = $this->get_settings_for_display();
$title = isset($settings['title']) ? sanitize_text_field((string) $settings['title']) : '';
$user_id = isset($settings['user_id']) ? absint($settings['user_id']) : 0;
$show_bio = (!empty($settings['show_bio']) && $settings['show_bio'] === 'yes');
$show_posts = (!empty($settings['show_posts']) && $settings['show_posts'] === 'yes');
$posts_count = isset($settings['posts_count']) ? absint($settings['posts_count']) : 3;
$posts_count = max(1, min(12, $posts_count));
$user = $user_id ? get_user_by('id', $user_id) : false;
echo '<div class="bpcab-author-box">';
if ($title !== '') {
echo '<div class="bpcab-author-box__title">' . esc_html($title) . '</div>';
}
if (!$user instanceof WP_User) {
// Fallback propre : évite une box vide.
echo '<p>' . esc_html__('Auteur introuvable (vérifiez l’ID utilisateur).', 'bpcab') . '</p>';
echo '</div>';
return;
}
$display_name = (string) $user->display_name;
$description = (string) get_user_meta((int) $user->ID, 'description', true);
$avatar = get_avatar((int) $user->ID, 96, '', $display_name, [
'class' => 'bpcab-author-box__avatar',
]);
// URL de profil optionnelle (Elementor URL control).
$profile_url = '';
$profile_is_external = false;
$profile_nofollow = false;
if (!empty($settings['profile_url']) && is_array($settings['profile_url'])) {
$profile_url = !empty($settings['profile_url']['url']) ? esc_url($settings['profile_url']['url']) : '';
$profile_is_external = !empty($settings['profile_url']['is_external']);
$profile_nofollow = !empty($settings['profile_url']['nofollow']);
}
echo '<div class="bpcab-author-box__header">';
echo '<div class="bpcab-author-box__avatar-wrap">' . $avatar . '</div>';
echo '<div class="bpcab-author-box__meta">';
$name_html = '<span class="bpcab-author-box__name">' . esc_html($display_name) . '</span>';
if ($profile_url) {
$rel = [];
if ($profile_is_external) {
// target blank sans noopener = vulnérable.
// Elementor gère souvent ça côté UI, mais on le force côté rendu.
$rel[] = 'noopener';
}
if ($profile_nofollow) {
$rel[] = 'nofollow';
}
$target = $profile_is_external ? ' target="_blank"' : '';
$rel_attr = !empty($rel) ? ' rel="' . esc_attr(implode(' ', array_unique($rel))) . '"' : '';
$name_html = '<a class="bpcab-author-box__name bpcab-author-box__name--link" href="' . esc_url($profile_url) . '"' . $target . $rel_attr . '>' . esc_html($display_name) . '</a>';
}
echo $name_html;
if ($show_bio && $description !== '') {
// Bio : autoriser un sous-ensemble HTML ? Ici on reste strict : texte simple.
echo '<div class="bpcab-author-box__bio">' . esc_html($description) . '</div>';
}
echo '</div>'; // meta
echo '</div>'; // header
if ($show_posts) {
$posts = get_posts([
'post_type' => 'post',
'post_status' => 'publish',
'author' => (int) $user->ID,
'numberposts' => $posts_count,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'suppress_filters' => false,
]);
echo '<div class="bpcab-author-box__posts">';
if (!empty($posts)) {
echo '<ul class="bpcab-author-box__posts-list">';
foreach ($posts as $post) {
$permalink = get_permalink($post);
$post_title = get_the_title($post);
echo '<li class="bpcab-author-box__posts-item">';
echo '<a class="bpcab-author-box__posts-link" href="' . esc_url($permalink) . '">' . esc_html($post_title) . '</a>';
echo '</li>';
}
echo '</ul>';
} else {
echo '<p class="bpcab-author-box__empty">' . esc_html__('Aucun article récent.', 'bpcab') . '</p>';
}
echo '</div>';
}
echo '</div>'; // box
}
// Optionnel : aperçu dans l’éditeur (JS template). On le laisse simple.
// Si vous ne le faites pas, Elementor affichera un rendu serveur en preview (souvent suffisant).
protected function content_template(): void {}
}
3) CSS du widget
Créez : wp-content/plugins/bpcab-elementor-widgets/assets/author-box.css
.bpcab-author-box{
display:block;
border-radius:12px;
background:#fff;
}
.bpcab-author-box__title{
font-weight:700;
margin:0 0 12px 0;
}
.bpcab-author-box__header{
display:flex;
gap:12px;
align-items:flex-start;
}
.bpcab-author-box__avatar{
border-radius:999px;
display:block;
}
.bpcab-author-box__meta{
display:block;
min-width:0;
}
.bpcab-author-box__name{
display:inline-block;
font-weight:700;
text-decoration:none;
}
.bpcab-author-box__bio{
margin-top:6px;
opacity:.9;
}
.bpcab-author-box__posts{
margin-top:14px;
}
.bpcab-author-box__posts-list{
margin:0;
padding-left:18px;
}
.bpcab-author-box__posts-item{
margin:6px 0;
}
4) JS (optionnel)
Créez : wp-content/plugins/bpcab-elementor-widgets/assets/author-box.js
/* Fichier volontairement vide.
Gardez-le si vous prévoyez d'ajouter des interactions.
Sinon, supprimez get_script_depends() et l'enregistrement du script. */
Explication du code
Ce qui se passe en coulisses (version simple)
Le plugin attend qu’Elementor soit chargé. Ensuite, il enregistre un widget. Elementor liste ce widget dans l’éditeur, et quand vous le déposez dans une page, Elementor :
- affiche vos contrôles (contenu + style),
- stocke les réglages dans le JSON de la page (post meta),
- appelle
render()pour produire le HTML côté front.
Pourquoi ces hooks et pas d’autres
plugins_loaded: bon moment pour vérifier l’environnement et la présence d’Elementor.did_action('elementor/loaded'): évite d’appeler des classes Elementor avant leur autoload.elementor/widgets/register: hook prévu pour enregistrer des widgets. C’est celui qui évite les “Class not found”.wp_register_style: on prépare les assets, mais on ne les enfile pas globalement.
Sanitization vs escaping (les erreurs que je vois le plus)
Deux règles :
- Sanitization : quand vous normalisez une valeur (ex :
absintpour un ID,sanitize_text_fieldpour un titre). - Escaping : quand vous imprimez dans le HTML (ex :
esc_html,esc_url,esc_attr).
Dans le widget, on sanitise les réglages au moment du rendu, puis on échappe systématiquement au moment d’afficher. Oui, Elementor stocke déjà des valeurs “propres” la plupart du temps, mais ne vous reposez pas dessus : un import JSON, un copier-coller, ou un plugin tiers peut injecter des chaînes inattendues.
Chargement conditionnel des assets
get_style_depends() retourne un handle de style enregistré. Elementor ne chargera ce style que si le widget est présent sur la page. C’est un pattern simple qui évite le CSS “site-wide”.
Requête des derniers articles
On utilise get_posts() avec :
no_found_rows: pas de pagination, donc pas besoin de compter.ignore_sticky_posts: évite des résultats surprenants.numberpostsborné à 12 : garde un widget “léger”.
Alternative possible : WP_Query si vous avez besoin de plus de contrôle, mais ici get_posts() suffit et reste lisible.
Variantes et cas d’usage
Variante 1 — Sélecteur d’auteur plus ergonomique (liste déroulante)
Le contrôle “ID utilisateur” est pratique pour un exemple, mais vos éditeurs vont vous maudire. Une variante simple : construire une liste d’options à partir des utilisateurs (attention aux sites avec des milliers d’utilisateurs).
Dans register_controls(), remplacez le contrôle user_id par :
<?php
// ✅ Variante : select (attention : coûteux si beaucoup d'utilisateurs).
$users = get_users([
'fields' => ['ID', 'display_name'],
'number' => 200, // borne volontaire
'orderby' => 'display_name',
'order' => 'ASC',
]);
$options = [];
foreach ($users as $u) {
$options[(string) $u->ID] = $u->display_name . ' (#' . $u->ID . ')';
}
$this->add_control(
'user_id',
[
'label' => esc_html__('Auteur', 'bpcab'),
'type' => Controls_Manager::SELECT,
'options' => $options,
'default' => (string) get_current_user_id(),
]
);
Edge case : sur un site membership avec 50 000 comptes, ce SELECT devient inutilisable. Dans ce cas, passez sur un contrôle AJAX (Select2) ou imposez un champ “ID” avec une aide UI (ou une recherche via REST).
Variante 2 — Afficher un CPT (ex : “portfolio”) au lieu des articles
Changez simplement 'post_type' => 'post' en 'post_type' => 'portfolio' (ou un tableau). Pensez à rendre le post type configurable via un SELECT si vous en avez plusieurs.
Variante 3 — Mise en cache légère (transient) par auteur
Si votre widget est utilisé 10 fois sur une page (ça arrive sur des landing pages “team”), vous pouvez cacher la liste des posts. Exemple simplifié :
<?php
$cache_key = 'bpcab_ab_posts_' . (int) $user->ID . '_' . (int) $posts_count;
$posts = get_transient($cache_key);
if ($posts === false) {
$posts = get_posts([
'post_type' => 'post',
'post_status' => 'publish',
'author' => (int) $user->ID,
'numberposts' => $posts_count,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
]);
// Cache 10 minutes.
set_transient($cache_key, $posts, 10 * MINUTE_IN_SECONDS);
}
Attention : si vous publiez souvent, le cache peut retarder l’affichage. Sur un site à fort trafic, vous préférerez un cache objet persistant (Redis) et des invalidations (hook save_post) plutôt qu’un TTL fixe.
Compatibilité Divi 5 / Elementor / Avada
Elementor (éditeur et front)
- Le widget apparaît dans la catégorie “Général”.
- Les styles sont scoped via
{{WRAPPER}}, ce qui limite les collisions CSS. - Si vous utilisez le cache Elementor / génération CSS, videz-le après modifications.
Divi 5
Divi n’utilise pas l’API Widget d’Elementor. Votre widget ne sera pas disponible dans Divi Builder.
Approche réaliste si vous devez supporter Divi 5 aussi :
- Exposez un shortcode “compat” qui réutilise la même logique PHP (fonction partagée), puis insérez-le via un module Code/Shortcode Divi.
- Ou créez un module Divi 5 dédié (plus propre, plus long).
Avada (Fusion Builder)
Même logique : Avada ne consomme pas les widgets Elementor. Pour Avada :
- Shortcode compatible (Fusion Builder sait les intégrer).
- Ou élément Fusion custom si vous avez besoin d’une UI native Avada.
Conseil “multi-builder” que j’applique souvent
Gardez la logique métier dans une classe PHP indépendante (ex : BPCAB_Author_Box_Renderer) et faites des adaptateurs :
- Widget Elementor → appelle le renderer.
- Shortcode → appelle le renderer.
- Bloc Gutenberg dynamique → appelle le renderer.
Vous évitez de dupliquer la logique de requête, les fallbacks, et l’escaping.
Vérifications après mise en place
- Activez le plugin dans Extensions.
- Ouvrez une page avec Elementor.
- Recherchez “Encart Auteur (BPCAB)”.
- Déposez le widget, entrez un ID utilisateur valide.
- Vérifiez côté front :
- avatar affiché,
- nom affiché (cliquable si URL fournie),
- bio affichée si activée,
- liste d’articles affichée et limitée.
- Testez les contrôles de style : couleur de fond, typographie du titre, padding.
Si vous avez un système de cache (plugin de cache, Cloudflare, cache serveur), purgez-le. J’ai souvent vu des développeurs croire que “le widget ne se charge pas”, alors que le front servait une page HTML cachée avant activation.
Si ça ne marche pas
Checklist rapide (dans l’ordre)
- Erreur 500 après activation : regardez
wp-content/debug.log(ou les logs serveur). - Widget invisible dans Elementor : Elementor est-il actif ? Le hook
elementor/widgets/registerest-il atteint ? - Class not found : vous avez probablement chargé la classe trop tôt, ou le chemin
require_onceest faux. - CSS non appliqué : le fichier est-il au bon chemin ? Le handle
bpcab-author-boxest-il bien enregistré ? Videz le cache Elementor. - Rien ne s’affiche : testez un ID utilisateur existant, puis désactivez “Afficher les derniers articles” pour isoler la requête.
Tableau de diagnostic
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Widget absent dans la liste Elementor | Hook non exécuté / Elementor non chargé | Regardez si did_action('elementor/loaded') est vrai (log), vérifiez Extensions |
Activez Elementor, évitez init, utilisez elementor/widgets/register |
| Erreur “Class ‘ElementorWidget_Base’ not found” | Classe chargée trop tôt | Stack trace dans debug.log |
Déplacez l’enregistrement sur elementor/widgets/register et gardez le require_once dedans |
| CSS non chargé | Handle non enregistré ou mauvais chemin | Onglet Network, cherchez author-box.css |
Corrigez plugins_url(), vérifiez get_style_depends(), purgez cache |
| Nom auteur affiché mais pas l’avatar | Gravatar bloqué / configuration avatar | Réglages > Discussion > Avatars, ou restrictions réseau | Autorisez Gravatar ou utilisez un avatar local (plugin) / fallback |
| Liste d’articles vide | Auteur sans posts publiés / mauvais post_type | Vérifiez l’auteur des posts et le statut “Publié” | Changez l’ID, publiez un post, ajustez post_type |
| Modifications non visibles | Cache navigateur / cache page / cache Elementor CSS | Test en navigation privée + purge cache plugin + regen CSS | Purger caches, régénérer fichiers CSS Elementor si activé |
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
Copier le code dans functions.php du thème |
Mauvais endroit : le thème peut changer, et l’ordre de chargement peut casser Elementor | Utilisez un plugin dédié (comme ici) ou un MU-plugin |
| Oublier un point-virgule dans la classe du widget | Erreur PHP fatale | Activez WP_DEBUG_LOG, relisez la ligne indiquée, utilisez un IDE |
Utiliser add_action('init', ...) pour enregistrer le widget |
Elementor pas encore chargé | Utilisez elementor/widgets/register et testez did_action('elementor/loaded') |
| CSS/JS “introuvable” | Mauvais chemin dans plugins_url() ou fichier non créé |
Vérifiez l’arborescence et le nom exact des fichiers |
| Conflit de styles avec le thème (Avada/Divi) | CSS trop générique (ex : .title) |
Préfixez vos classes (bpcab-) et utilisez {{WRAPPER}} dans les selectors Elementor |
| Erreur après mise à jour PHP | Typage strict + code ancien incompatible | Gardez PHP 8.1+ et corrigez les warnings/TypeError (logs) |
| Tester directement en production | Risque d’écran blanc | Staging + déploiement versionné + rollback |
| Confusion entre sanitization et escaping | Données “propres” supposées | Sanitisez les entrées, échappez les sorties, systématiquement |
Conseils sécurité, performance et maintenance
- Sécurité XSS : échappez chaque sortie selon le contexte (
esc_htmltexte,esc_urlURL,esc_attrattribut). Référence : developer.wordpress.org/apis/security/escaping - Permissions : ici, pas de formulaire front, donc pas de nonce. Si vous ajoutez des actions (ex : bouton “suivre l’auteur”), utilisez
wp_nonce_fieldet vérifiezcurrent_user_can. - Performance : limitez le nombre de posts, activez un cache si le widget est répété. Sur gros sites, évitez
get_users()sans limites. - Compatibilité : gardez vos classes et handles préfixés. C’est la meilleure défense contre les collisions de noms avec d’autres add-ons Elementor.
- Maintenance : versionnez le plugin (Git), et mettez un changelog. Évitez les snippets “collés” dans un plugin de snippets : j’ai vu des mises à jour casser des classes autoloadées parce que le plugin de snippets changeait l’ordre d’exécution.
- SEO : ce widget n’ajoute pas de contenu caché, donc pas de piège SEO. Faites attention si vous ajoutez du contenu conditionnel via JS uniquement.
Ressources
- WordPress Plugin Developer Handbook
- WordPress Security APIs (validation, sanitization, escaping)
- Référence wp_register_style()
- Référence get_posts()
- Référence get_avatar()
- Page officielle Elementor sur wordpress.org
- Miroir GitHub WordPress core (wordpress-develop)
- WordPress Core Trac
- Notes de version PHP 8.1
FAQ
Pourquoi créer un plugin plutôt que mettre le code dans le thème enfant ?
Parce qu’un widget Elementor est une fonctionnalité. Si vous changez de thème (ou si Avada/Divi est remplacé), vous voulez garder le widget. Et vous évitez aussi des surprises d’ordre de chargement.
Est-ce que ce widget fonctionne si Elementor Pro n’est pas installé ?
Oui. Le code utilise l’API widget d’Elementor (version gratuite). Elementor Pro apporte d’autres fonctionnalités (Theme Builder, etc.), mais ce widget n’en dépend pas.
Pourquoi ne pas utiliser un shortcode dans un widget “Shortcode” Elementor ?
Vous perdez la plupart des contrôles de style natifs, et vous finissez par injecter du CSS global. Pour un composant récurrent, un widget est plus propre.
Comment ajouter une catégorie “BPCAB” dans Elementor au lieu de “Général” ?
Vous pouvez enregistrer une catégorie Elementor dédiée via les hooks de catégories (selon version Elementor). Je le fais quand j’ai 5+ widgets, sinon je reste sur “Général” pour éviter de fragmenter l’UI.
Pourquoi get_settings_for_display() et pas get_settings() ?
get_settings_for_display() renvoie les valeurs prêtes pour l’affichage (avec certaines transformations internes Elementor). C’est généralement le bon choix dans render().
Comment éviter la saisie d’un ID utilisateur (pas user-friendly) ?
Utilisez un SELECT borné (200 utilisateurs max) ou implémentez un contrôle AJAX. Sur les sites à gros volume, un champ ID + documentation interne est parfois la solution la plus stable.
Pourquoi mon CSS ne se met pas à jour après modification ?
Cache. Videz :
- cache navigateur,
- cache de votre plugin de cache,
- et si vous utilisez la génération CSS d’Elementor, forcez la régénération (selon votre configuration).
Peut-on utiliser ce widget dans un template Elementor Theme Builder (single post) ?
Oui. C’est même un bon cas d’usage : un encart auteur en bas d’article. Dans ce cas, améliorez la logique pour prendre l’auteur du post courant si user_id est vide (variante simple à ajouter).
Comment adapter le widget pour afficher l’auteur du post courant automatiquement ?
Dans render(), si $user_id vaut 0, récupérez get_post_field('post_author', get_the_ID()) quand vous êtes dans une boucle/template. Faites attention aux pages statiques sans contexte de post.
Est-ce que ce code est compatible PHP 8.2/8.3/8.4 ?
Oui en principe (syntaxe moderne, typage strict). Le point de vigilance, c’est surtout la compatibilité Elementor et les warnings dépréciés venant d’autres plugins.
Comment tester proprement sans casser le site ?
Staging, logs activés, et une méthode simple : activez le plugin, déposez le widget sur une page de test, vérifiez le front, puis testez un cas “auteur introuvable” (ID inexistant) pour valider les fallbacks.