Le problème / Le besoin

Si vous avez déjà livré un site Elementor et qu’un client vous demande “je veux un widget sur mesure, mais sans installer 12 add-ons”, vous avez déjà le bon réflexe : passer par le code.

Elementor (Free/Pro) expose une API de hooks et d’événements suffisamment stable pour personnaliser le builder proprement : ajouter une catégorie de widgets, enregistrer un widget custom, injecter des contrôles, forcer des valeurs par défaut, charger des scripts uniquement quand c’est utile, et même ajouter un champ dynamique via un tag.

Le besoin métier typique : industrialiser des composants réutilisables (CTA, encarts auteur, fiches produit, tableaux, bandeaux RGPD, etc.) tout en gardant l’UX d’Elementor. À la fin, vous saurez structurer un mini-plugin “propre” compatible WordPress 6.9.4+ et PHP 8.1+, et vous aurez une base réutilisable pour vos projets.

Résumé rapide

  • Créer un mini-plugin WordPress (pas un snippet jetable) qui s’intègre à Elementor sans casser l’admin.
  • Utiliser les bons hooks Elementor : elementor/init, elementor/widgets/register, elementor/elements/categories_registered, elementor/frontend/after_register_scripts.
  • Ajouter un widget custom “Badge” (titre + texte + couleur + icône) avec contrôles, rendu sécurisé et styles.
  • Ajouter un Dynamic Tag (option avancée) pour injecter une valeur depuis les meta utilisateur (ex: poste/role).
  • Charger CSS/JS uniquement si un widget de votre plugin est présent (évite le “tout charger partout”).

Quand utiliser cette solution

  • Vous voulez un composant stable, versionné, réutilisable sur plusieurs sites (agence, freelance, équipe).
  • Vous devez respecter une charte (couleurs, typographies, spacing) sans laisser 50 options “dangereuses” au client.
  • Vous avez besoin d’un rendu précis côté front, sans dépendre d’un add-on tiers qui peut changer sans prévenir.
  • Vous voulez améliorer la performance : assets chargés uniquement si nécessaire, pas de “gros pack” de widgets.
  • Vous souhaitez intégrer des données WordPress (meta, options, ACF/Pods, etc.) via des tags dynamiques.

Quand ne PAS utiliser cette solution

  • Le besoin est purement visuel et ponctuel : un simple template Elementor, un conteneur, et un peu de CSS peuvent suffire.
  • Vous n’avez pas la main sur la maintenance : un widget custom implique de suivre Elementor (et parfois ses dépréciations).
  • Vous cherchez à “patcher” Elementor en profondeur (ex: modifier le comportement interne de l’éditeur) : c’est rarement stable. Préférez des extensions officielles, ou acceptez une dette technique.
  • Votre client utilise majoritairement Gutenberg/blocks : dans ce cas, un bloc custom (Block API) est souvent plus cohérent. Voir la doc officielle : Block Editor Handbook.

Prérequis / avant de commencer

  • WordPress 6.9.4+ et PHP 8.1+ (idéalement 8.2/8.3 en 2026 si votre hébergeur suit).
  • Elementor installé et activé (Free suffit pour l’exemple widget). Pour les Dynamic Tags avancés, Elementor Pro est souvent utilisé, mais on reste sur des APIs publiques quand possible.
  • Un environnement de test (staging) + sauvegarde avant toute modification. J’ai souvent vu des snippets collés en production qui déclenchent un fatal et bloquent l’admin.
  • Un plugin de logs (ou au minimum WP_DEBUG_LOG) pour lire les erreurs PHP.

Références utiles côté WordPress :

L’approche naïve (et pourquoi l’éviter)

Le classique : coller un gros bout de code dans functions.php du thème (souvent sans thème enfant), enregistrer des scripts partout, et instancier des classes Elementor dès le chargement.

Exemple typique (anti-pattern)

<?php
// ❌ Exemple volontairement mauvais : ne copiez pas tel quel.

add_action('init', function () {
    // ❌ Elementor n'est pas forcément chargé ici, et cette classe peut ne pas exister.
    $widgets_manager = ElementorPlugin::instance()->widgets_manager;

    require_once __DIR__ . '/widgets/badge.php';
    $widgets_manager->register(new My_Badge_Widget());

    // ❌ Charge CSS/JS sur toutes les pages, même si le widget n'est pas utilisé.
    wp_enqueue_style('my-badge', get_stylesheet_directory_uri() . '/badge.css');
});

Pourquoi ça casse (souvent)

  • Timing : Elementor n’a pas fini d’initialiser ses managers au moment de init (selon versions / contextes).
  • Fatal error : si Elementor est désactivé, ElementorPlugin n’existe pas.
  • Performance : CSS/JS chargés partout, y compris sur des pages qui n’utilisent pas Elementor.
  • Maintenance : code perdu dans le thème, impossible à versionner proprement, fragile lors d’un changement de thème.

La bonne approche — tutoriel pas à pas

On va faire un mini-plugin, avec :

  • un bootstrap qui vérifie qu’Elementor est actif,
  • une catégorie de widgets dédiée,
  • un widget custom,
  • un chargement d’assets conditionnel,
  • une variante “Dynamic Tag” (optionnelle) pour illustrer un filtre/registre avancé.

Étape 1 — Créez le plugin

Créez ce dossier : wp-content/plugins/bpcab-elementor-hooks/

Puis ce fichier : wp-content/plugins/bpcab-elementor-hooks/bpcab-elementor-hooks.php

Étape 2 — Bootstrap + vérification Elementor

On accroche notre code à plugins_loaded puis on attend elementor/init. Le point clé : ne jamais appeler des classes Elementor tant que le plugin n’est pas prêt.

Étape 3 — Enregistrer une catégorie + un widget

Elementor expose des actions dédiées. En pratique, celles-ci sont stables depuis plusieurs versions :

  • elementor/elements/categories_registered pour ajouter une catégorie,
  • elementor/widgets/register pour enregistrer un widget.

J’insiste : évitez d’utiliser des hooks “au hasard” (comme init ou wp_loaded) pour toucher à Elementor. Le problème vient rarement du code du widget, mais du moment où il s’exécute.

Étape 4 — Charger CSS/JS au bon moment

On enregistre les assets via elementor/frontend/after_register_styles / elementor/frontend/after_register_scripts, puis on les enqueue seulement si le widget est réellement rendu.

Étape 5 — (Option) Ajouter un Dynamic Tag

Si vous utilisez Elementor Pro (ou si votre stack supporte les tags dynamiques), un tag custom est souvent plus propre qu’un shortcode. Vous exposez une donnée, Elementor gère l’injection dans ses contrôles “Dynamic”.

Code complet

Copiez-collez l’ensemble ci-dessous. Le plugin est autonome, et vous pourrez ajouter des widgets ensuite.

Fichier 1 — bpcab-elementor-hooks.php

<?php
/**
 * Plugin Name: BPCAB - Personnalisation Elementor par hooks
 * Description: Exemple pédagogique : catégorie + widget custom + assets conditionnels + (option) Dynamic Tag.
 * Version: 1.0.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 * Author: BPCAB
 * License: GPLv2 or later
 */

declare(strict_types=1);

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

final class BPCAB_Elementor_Hooks_Plugin {

	public const VERSION = '1.0.0';
	public const SLUG    = 'bpcab-elementor-hooks';

	private static ?self $instance = null;

	public static function instance(): self {
		if (null === self::$instance) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {
		add_action('plugins_loaded', [$this, 'bootstrap']);
	}

	public function bootstrap(): void {
		// Elementor définit généralement ELEMENTOR_VERSION quand il est actif.
		if (!defined('ELEMENTOR_VERSION')) {
			// Pas d'Elementor : on ne fait rien. Évitez d'afficher une notice agressive en front.
			add_action('admin_notices', [$this, 'admin_notice_missing_elementor']);
			return;
		}

		// On attend l'initialisation d'Elementor avant d'appeler ses classes/managers.
		add_action('elementor/init', [$this, 'on_elementor_init']);
	}

	public function admin_notice_missing_elementor(): void {
		if (!current_user_can('activate_plugins')) {
			return;
		}

		$plugin_name = esc_html__('BPCAB - Personnalisation Elementor par hooks', 'bpcab');
		$message     = esc_html__('Elementor doit être activé pour utiliser ce plugin.', 'bpcab');

		echo '<div class="notice notice-warning">';
		echo '<p><strong>' . $plugin_name . '</strong> — ' . $message . '</p>';
		echo '</div>';
	}

	public function on_elementor_init(): void {
		// 1) Catégorie de widgets.
		add_action('elementor/elements/categories_registered', [$this, 'register_category']);

		// 2) Widgets.
		add_action('elementor/widgets/register', [$this, 'register_widgets']);

		// 3) Assets : on les enregistre au bon moment côté front.
		add_action('elementor/frontend/after_register_styles', [$this, 'register_frontend_styles']);
		add_action('elementor/frontend/after_register_scripts', [$this, 'register_frontend_scripts']);

		// 4) Option : Dynamic Tag (si l'API est disponible).
		add_action('elementor/dynamic_tags/register', [$this, 'register_dynamic_tags']);
	}

	public function register_category($elements_manager): void {
		// $elements_manager est typiquement une instance de ElementorElements_Manager.
		$elements_manager->add_category(
			'bpcab',
			[
				'title' => esc_html__('BPCAB', 'bpcab'),
				'icon'  => 'fa fa-plug',
			]
		);
	}

	public function register_widgets($widgets_manager): void {
		// Chargement des classes de widgets.
		require_once __DIR__ . '/includes/widgets/class-bpcab-widget-badge.php';

		// Enregistrement.
		$widgets_manager->register(new BPCAB_Widget_Badge());
	}

	public function register_frontend_styles(): void {
		wp_register_style(
			'bpcab-badge',
			plugins_url('assets/css/badge.css', __FILE__),
			[],
			self::VERSION
		);
	}

	public function register_frontend_scripts(): void {
		wp_register_script(
			'bpcab-badge',
			plugins_url('assets/js/badge.js', __FILE__),
			[],
			self::VERSION,
			true
		);
	}

	public function register_dynamic_tags($dynamic_tags_manager): void {
		// Certains sites n'utilisent pas cette feature : on protège le require.
		require_once __DIR__ . '/includes/dynamic-tags/class-bpcab-dynamic-tag-user-position.php';

		// Enregistrement du tag.
		$dynamic_tags_manager->register(new BPCAB_Dynamic_Tag_User_Position());
	}
}

BPCAB_Elementor_Hooks_Plugin::instance();

Fichier 2 — includes/widgets/class-bpcab-widget-badge.php

<?php
declare(strict_types=1);

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

use ElementorWidget_Base;
use ElementorControls_Manager;
use ElementorIcons_Manager;

final class BPCAB_Widget_Badge extends Widget_Base {

	public function get_name(): string {
		return 'bpcab_badge';
	}

	public function get_title(): string {
		return esc_html__('Badge (BPCAB)', 'bpcab');
	}

	public function get_icon(): string {
		return 'eicon-badge';
	}

	public function get_categories(): array {
		return ['bpcab'];
	}

	public function get_keywords(): array {
		return ['badge', 'label', 'cta', 'bpcab'];
	}

	public function get_style_depends(): array {
		// Elementor enqueuera ce style seulement si le widget est présent.
		return ['bpcab-badge'];
	}

	public function get_script_depends(): array {
		// Idem pour le script.
		return ['bpcab-badge'];
	}

	protected function register_controls(): void {

		$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__('Nouveau', 'bpcab'),
				'placeholder' => esc_html__('Ex: Nouveau', 'bpcab'),
				'label_block' => true,
			]
		);

		$this->add_control(
			'text',
			[
				'label'       => esc_html__('Texte', 'bpcab'),
				'type'        => Controls_Manager::TEXTAREA,
				'default'     => esc_html__('Offre limitée', 'bpcab'),
				'placeholder' => esc_html__('Ex: Offre limitée', 'bpcab'),
				'rows'        => 3,
			]
		);

		$this->add_control(
			'icon',
			[
				'label'   => esc_html__('Icône', 'bpcab'),
				'type'    => Controls_Manager::ICONS,
				'default' => [
					'value'   => 'fas fa-star',
					'library' => 'fa-solid',
				],
			]
		);

		$this->end_controls_section();

		$this->start_controls_section(
			'section_style',
			[
				'label' => esc_html__('Style', 'bpcab'),
				'tab'   => Controls_Manager::TAB_STYLE,
			]
		);

		$this->add_control(
			'bg_color',
			[
				'label'     => esc_html__('Couleur de fond', 'bpcab'),
				'type'      => Controls_Manager::COLOR,
				'default'   => '#111827',
				'selectors' => [
					'{{WRAPPER}} .bpcab-badge' => 'background-color: {{VALUE}};',
				],
			]
		);

		$this->add_control(
			'text_color',
			[
				'label'     => esc_html__('Couleur du texte', 'bpcab'),
				'type'      => Controls_Manager::COLOR,
				'default'   => '#ffffff',
				'selectors' => [
					'{{WRAPPER}} .bpcab-badge' => 'color: {{VALUE}};',
				],
			]
		);

		$this->add_responsive_control(
			'padding',
			[
				'label'      => esc_html__('Padding', 'bpcab'),
				'type'       => Controls_Manager::DIMENSIONS,
				'size_units' => ['px', 'em', 'rem'],
				'default'    => [
					'top'    => 12,
					'right'  => 14,
					'bottom' => 12,
					'left'   => 14,
					'unit'   => 'px',
				],
				'selectors'  => [
					'{{WRAPPER}} .bpcab-badge' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
				],
			]
		);

		$this->end_controls_section();
	}

	protected function render(): void {
		$settings = $this->get_settings_for_display();

		// Sanitization/escaping : Elementor stocke des valeurs, mais vous devez sortir du HTML propre.
		$title = isset($settings['title']) ? sanitize_text_field((string) $settings['title']) : '';
		$text  = isset($settings['text']) ? wp_kses_post((string) $settings['text']) : '';

		// Icône : Elementor fournit Icons_Manager pour rendre proprement.
		$icon = $settings['icon'] ?? null;

		echo '<div class="bpcab-badge" role="note">';
		echo '<div class="bpcab-badge__head">';

		if (!empty($icon) && is_array($icon)) {
			echo '<span class="bpcab-badge__icon" aria-hidden="true">';
			Icons_Manager::render_icon($icon, ['aria-hidden' => 'true']);
			echo '</span>';
		}

		if ($title !== '') {
			echo '<strong class="bpcab-badge__title">' . esc_html($title) . '</strong>';
		}

		echo '</div>';

		if ($text !== '') {
			// wp_kses_post permet un sous-ensemble HTML (liens, strong, em, etc.).
			echo '<div class="bpcab-badge__text">' . $text . '</div>';
		}

		echo '</div>';
	}
}

Fichier 3 — includes/dynamic-tags/class-bpcab-dynamic-tag-user-position.php

<?php
declare(strict_types=1);

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

use ElementorCoreDynamicTagsTag;

final class BPCAB_Dynamic_Tag_User_Position extends Tag {

	public function get_name(): string {
		return 'bpcab-user-position';
	}

	public function get_title(): string {
		return esc_html__('Utilisateur : Poste (BPCAB)', 'bpcab');
	}

	public function get_group(): string {
		// Groupe "Site" ou "User" selon votre organisation.
		return 'site';
	}

	public function get_categories(): array {
		// Catégorie TEXT pour insertion dans des champs texte.
		return [ElementorModulesDynamicTagsModule::TEXT_CATEGORY];
	}

	protected function register_controls(): void {
		// Exemple : choisir une meta key (simple). En prod, vous pourriez proposer une liste.
		$this->add_control(
			'meta_key',
			[
				'label'       => esc_html__('Meta key utilisateur', 'bpcab'),
				'type'        => ElementorControls_Manager::TEXT,
				'default'     => 'position',
				'placeholder' => 'position',
			]
		);
	}

	public function render(): void {
		$user_id = get_current_user_id();
		if (!$user_id) {
			return;
		}

		$settings = $this->get_settings();
		$meta_key = isset($settings['meta_key']) ? sanitize_key((string) $settings['meta_key']) : 'position';

		$value = get_user_meta($user_id, $meta_key, true);
		if (!is_scalar($value) || $value === '') {
			return;
		}

		echo esc_html((string) $value);
	}
}

Fichier 4 — assets/css/badge.css

.bpcab-badge{
	display:block;
	border-radius:12px;
	line-height:1.35;
}
.bpcab-badge__head{
	display:flex;
	gap:10px;
	align-items:center;
}
.bpcab-badge__icon{
	display:inline-flex;
}
.bpcab-badge__title{
	font-weight:700;
}
.bpcab-badge__text{
	margin-top:8px;
	opacity:.95;
}

Fichier 5 — assets/js/badge.js

(function () {
	// Script minimal : exemple de point d'accroche.
	// Ici, on ne fait rien de critique. Gardez vos widgets robustes sans JS si possible.
})();

Explication du code

1) Pourquoi un plugin (et pas functions.php)

Un plugin vous donne un cycle de vie clair, une activation/désactivation, une version, et un endroit stable pour vos classes. J’ai souvent vu des sites Avada/Divi mis à jour, et un “petit snippet” dans le thème disparaît ou devient incompatible.

2) Le point clé : le timing des hooks

  • plugins_loaded : WordPress a chargé les plugins. On peut vérifier si Elementor est là.
  • elementor/init : Elementor a initialisé son conteneur principal. C’est ici que vous ajoutez vos hooks Elementor.
  • elementor/widgets/register : vous recevez le manager des widgets et vous enregistrez vos classes.
  • elementor/elements/categories_registered : vous déclarez une catégorie visible dans l’UI du builder.

Ce découpage évite le bug classique : “Class ‘ElementorPlugin’ not found” ou “Call to a member function register() on null”.

3) Chargement conditionnel des assets

Le duo get_style_depends() / get_script_depends() est sous-utilisé. Pourtant, c’est l’un des moyens les plus propres de charger vos assets uniquement quand Elementor rend votre widget.

En coulisses : Elementor collecte les dépendances des widgets présents sur la page et enqueuera les handles correspondants. Vous, vous vous contentez de wp_register_style() / wp_register_script() au bon moment.

4) Rendu sécurisé : sanitization + escaping

  • Entrée : Elementor stocke des valeurs en DB. Vous devez quand même les nettoyer selon le contexte.
  • Sortie : esc_html() pour du texte, wp_kses_post() si vous autorisez un HTML limité.

Le piège que je vois le plus : sortir directement $settings['text'] sans wp_kses_post() “parce que c’est l’admin”. Sur un site multi-auteurs, ça devient un risque XSS.

5) Dynamic Tag : pourquoi c’est utile

Un tag dynamique vous évite les shortcodes dans les champs Elementor. Vous exposez une donnée (meta user, option, champ ACF), et l’utilisateur sélectionne le tag dans l’UI. C’est plus maintenable qu’un shortcode collé dans 30 widgets.

Variantes et cas d’usage

Variante 1 — Forcer des valeurs “verrouillées” (moins d’options client)

Si vous voulez éviter que le client change certaines options, vous pouvez :

  • ne pas exposer le contrôle (pas de add_control),
  • ou exposer une liste fermée (SELECT),
  • ou imposer une valeur en render().

Exemple : imposer une classe CSS selon un “type” :

// Dans register_controls()
$this->add_control(
	'type',
	[
		'label'   => esc_html__('Type', 'bpcab'),
		'type'    => Controls_Manager::SELECT,
		'default' => 'info',
		'options' => [
			'info'    => esc_html__('Info', 'bpcab'),
			'warning' => esc_html__('Alerte', 'bpcab'),
		],
	]
);

// Dans render()
$type = isset($settings['type']) ? sanitize_key((string) $settings['type']) : 'info';
$type_class = in_array($type, ['info', 'warning'], true) ? 'is-' . $type : 'is-info';
echo '<div class="bpcab-badge ' . esc_attr($type_class) . '">...</div>';

Variante 2 — Ajouter un contrôle URL et rendre un lien propre

Un CTA “Badge cliquable” arrive tout le temps. Elementor fournit un contrôle URL avec options “ouvrir dans un nouvel onglet” et “nofollow”.

// Contrôle URL
$this->add_control(
	'link',
	[
		'label' => esc_html__('Lien', 'bpcab'),
		'type'  => Controls_Manager::URL,
		'options' => ['url', 'is_external', 'nofollow'],
		'default' => [
			'url' => '',
		],
	]
);

// Dans render()
$link = $settings['link'] ?? [];
$url  = isset($link['url']) ? esc_url((string) $link['url']) : '';

if ($url) {
	$target = !empty($link['is_external']) ? ' target="_blank"' : '';
	$rel    = !empty($link['nofollow']) ? ' rel="nofollow noopener"' : ' rel="noopener"';

	echo '<a class="bpcab-badge" href="' . $url . '"' . $target . $rel . '>...</a>';
	return;
}

Note : si vous ouvrez un lien en nouvel onglet, gardez noopener (sécurité).

Variante 3 — Charger un asset uniquement sur certaines pages (encore plus strict)

Si vous avez un script lourd, vous pouvez combiner la dépendance widget + une condition WordPress. Exemple : seulement sur les pages (pas les articles) :

public function register_frontend_scripts(): void {
	wp_register_script(
		'bpcab-badge',
		plugins_url('assets/js/badge.js', __FILE__),
		[],
		self::VERSION,
		true
	);

	// ⚠️ Ne faites pas wp_enqueue_script ici : Elementor gère l'enqueue via get_script_depends().
	// Si vous voulez vraiment empêcher le chargement sur certains contextes, vous pouvez deregister :
	if (!is_page()) {
		wp_deregister_script('bpcab-badge');
	}
}

Je l’utilise rarement : ça peut surprendre si un widget est utilisé dans un template qui s’affiche ailleurs. Testez bien.

Compatibilité Divi 5 / Elementor / Avada

Elementor

  • Le plugin ci-dessus s’intègre “dans” Elementor, sans dépendre d’un thème.
  • Si vous utilisez des templates Elementor (Theme Builder), le widget reste disponible partout.
  • Les assets sont conditionnels : bon point sur des sites très chargés.

Divi 5

Divi 5 n’utilise pas l’API Elementor. Votre widget n’apparaîtra pas dans Divi, et c’est normal.

Si votre objectif est de réutiliser le même composant sur des pages Divi, je recommande une stratégie “agnostique builder” :

  • créer un shortcode WordPress (ou mieux : un bloc Gutenberg),
  • puis l’insérer dans Divi via un module Code/Shortcode,
  • et garder Elementor comme une “surcouche UI” quand il est présent.

Dans mon expérience, c’est la seule approche qui tient si vous avez un parc multi-builders.

Avada (Fusion Builder)

Même logique : Avada ne consommera pas les widgets Elementor. Par contre, votre plugin reste utile si le site a Elementor sur certaines pages.

Pour Avada, le pattern le plus propre est aussi : shortcode ou bloc, puis élément “Code Block” / “Shortcode” dans Fusion Builder.

Vérifications après mise en place

  1. Activez le plugin dans Extensions.
  2. Ouvrez une page avec Elementor.
  3. Dans le panneau des widgets, cherchez la catégorie BPCAB puis le widget Badge (BPCAB).
  4. Déposez-le sur la page, modifiez le titre/texte/couleurs, et publiez.
  5. Sur le front, inspectez la page : vous devez voir bpcab-badge.css chargé (et pas sur les pages qui n’utilisent pas le widget).

Tableau de diagnostic rapide

Symptôme Cause probable Vérification Solution
La catégorie BPCAB n’apparaît pas Hook jamais exécuté (Elementor non chargé) Vérifier que ELEMENTOR_VERSION est défini, et qu’Elementor est actif Activer Elementor, vérifier conflits/mu-plugins
Fatal error “Class ElementorWidget_Base not found” Le fichier widget est chargé trop tôt / Elementor inactif Regarder le log PHP + stack trace Ne require le widget que dans elementor/widgets/register après elementor/init
CSS/JS pas chargés Handles non enregistrés ou mauvais hook d’enregistrement Inspecter wp_head / wp_footer + console Vérifier after_register_styles/scripts et get_style_depends()
Le widget s’affiche, mais styles cassés Cache (plugin/CDN) ou minification agressive Désactiver cache, vider CDN, tester en navigation privée Exclure les fichiers du cache/minify, bump version
Dynamic Tag introuvable Feature non dispo (selon config/Pro) ou hook non déclenché Vérifier si le panneau “Dynamic” existe sur un champ texte Installer/activer Elementor Pro si nécessaire, ou retirer la partie tag

Si ça ne marche pas

  1. Confirmez les versions : WordPress 6.9.4+, PHP 8.1+, Elementor à jour. Un vieux PHP déclenche des erreurs sur declare(strict_types=1) ou les types ?self.
  2. Activez les logs dans wp-config.php (en staging) :

    define('WP_DEBUG', true);

    define('WP_DEBUG_LOG', true);

    define('WP_DEBUG_DISPLAY', false);
  3. Ouvrez wp-content/debug.log et cherchez “BPCAB” ou “Elementor”.
  4. Désactivez temporairement les plugins de snippets. J’ai déjà vu un snippet “ancien tuto Elementor” déclarer une classe du même nom et provoquer un conflit fatal.
  5. Videz les caches : cache plugin, cache serveur, CDN, cache navigateur. Sur Elementor, un cache agressif peut aussi conserver des assets manquants.
  6. Regénérez les CSS Elementor (si votre site utilise l’option de génération CSS). Dans Elementor, vous avez généralement une action de régénération dans les outils/performances.
  7. Testez avec un thème neutre (temporaires) : Twenty Twenty-* ou un thème léger. Un thème peut deregister des scripts/styles.

Pièges et erreurs courantes

Erreur Cause Solution
Code collé dans le mauvais fichier Ajout dans functions.php au lieu d’un plugin Créer un plugin, versionner, activer/désactiver proprement
“Parse error: syntax error” Point-virgule manquant, accolade en trop, copier-coller incomplet Relire la ligne indiquée dans le log, utiliser un IDE avec formatage PHP
Hook Elementor inadapté Utilisation de init / wp_loaded pour enregistrer un widget Utiliser elementor/init puis elementor/widgets/register
“Class ElementorPlugin not found” Elementor désactivé ou chargé après votre code Vérifier defined('ELEMENTOR_VERSION') et ne jamais appeler Elementor avant elementor/init
CSS/JS non chargés Mauvais handle, mauvais hook, ou cache/minification Enregistrer via after_register_styles/scripts, déclarer les dépendances via get_*_depends(), vider cache
Conflit de nom de classe Deux plugins déclarent BPCAB_Widget_Badge (ou un autoloader mal configuré) Préfixer systématiquement, utiliser namespaces si vous industrialisez
Confusion action vs filtre Vous essayez de “return” dans une action Actions : effets de bord. Filtres : retour d’une valeur. Relire le hook utilisé
Test direct en production Pas de staging, pas de backup Staging + sauvegarde + plan de rollback (désactivation plugin via FTP si besoin)
Permaliens / templates incohérents Vous testez sur un template différent de celui rendu (Theme Builder) Vérifier quel template Elementor est réellement appliqué, purger caches

Conseils sécurité, performance et maintenance

Sécurité

  • Escaping systématique : tout ce qui sort en HTML doit être échappé selon le contexte (esc_html, esc_attr, esc_url, wp_kses_post). Référence : WordPress: Data Validation.
  • Pas d’options “HTML libre” pour des rôles non-admin. Sur des sites multi-auteurs, c’est un vecteur XSS.
  • Pas d’exécution PHP via widget (ça paraît évident, mais j’ai déjà vu des “widgets code” bricolés).

Performance

  • Assets conditionnels via get_style_depends() / get_script_depends() : c’est le meilleur ratio effort/gain.
  • Évitez les requêtes en boucle dans render(). Si vous devez charger des données (posts, meta), cachez (transients/object cache) ou préparez via query optimisée.
  • CSS minimal : un widget = un petit fichier. Si vous en avez 20, regroupez intelligemment (mais gardez la conditionnalité).

Maintenance

  • Versionnez le plugin (Git) et taggez vos releases. Quand Elementor change une API, vous saurez quoi déployer.
  • Évitez les tutos “anciens” qui utilisent des hooks obsolètes. Si vous reprenez un snippet de 2021-2023, vérifiez qu’il colle à Elementor actuel et à WordPress 6.9.4.
  • Préparez une stratégie de fallback : si Elementor est désactivé, votre plugin doit “ne rien faire” sans fatal.

Ressources

FAQ

Est-ce que ce code fonctionne avec WordPress 6.9.4 ?

Oui : le plugin suit les pratiques standard WordPress (hooks, enqueue) et cible PHP 8.1+. Le point de compatibilité principal reste la version d’Elementor (gardez-la à jour).

Pourquoi ne pas utiliser un plugin de snippets ?

Pour un test rapide, ça passe. Pour un widget Elementor réutilisable, un vrai plugin est plus fiable : chargement maîtrisé, fichiers organisés, versioning, et désactivation propre si ça casse.

Mon widget apparaît, mais il n’est pas dans la bonne catégorie

Vérifiez que get_categories() retourne bien ['bpcab'], et que la catégorie est enregistrée via elementor/elements/categories_registered.

Comment ajouter plusieurs widgets ?

Ajoutez d’autres fichiers dans includes/widgets/ et enregistrez-les dans register_widgets(). Gardez un fichier = une classe.

Comment éviter de charger le JS si je n’en ai pas besoin ?

Supprimez get_script_depends() ou renvoyez un tableau vide. Gardez le widget fonctionnel sans JS autant que possible.

Peut-on utiliser un autoloader (Composer) ?

Oui, surtout si vous avez 10+ widgets. En contexte WordPress, faites attention à ne pas imposer Composer au site final. Une approche courante : autoloader PSR-4 embarqué dans le plugin.

Pourquoi utiliser wp_kses_post() pour le texte ?

Parce qu’un textarea peut contenir du HTML si Elementor le permet (ou si l’utilisateur colle du contenu). wp_kses_post() autorise un sous-ensemble sûr, contrairement à un echo brut.

Le Dynamic Tag ne s’affiche pas : c’est normal ?

Ça dépend de votre configuration Elementor. Vérifiez que l’UI “Dynamic” est disponible sur vos champs. Si votre site ne supporte pas les tags dynamiques, retirez la partie elementor/dynamic_tags/register et le dossier associé.

Comment tester proprement sans casser l’éditeur ?

Testez en staging, activez les logs, et commencez par un widget minimal (rendu + un contrôle). Ajoutez les contrôles un par un. Les erreurs Elementor sont souvent silencieuses côté UI, mais visibles dans la console et le log PHP.

Est-ce compatible avec un thème enfant Divi/Avada ?

Oui, parce que c’est un plugin. Par contre, le widget n’apparaîtra que dans Elementor. Pour Divi/Avada, utilisez plutôt un shortcode ou un bloc si vous voulez un composant partagé entre builders.