Si vous avez déjà voulu un “vrai” élément Avada (Fusion Builder) réutilisable, avec ses options dans l’UI, mais que vous vous êtes retrouvé à coller un shortcode bricolé dans un bloc Code… vous connaissez le problème : c’est fragile, difficile à maintenir, et ça casse au prochain refactor.

Le problème / Le besoin

Sur des sites Avada, je vois souvent le même scénario : un client veut un composant répétable (ex. “Encart promo”, “Carte auteur”, “Bandeau CTA”) avec 6–10 options, un rendu propre, et une intégration naturelle dans Fusion Builder (édition visuelle, duplication, presets, etc.).

Un simple shortcode peut suffire, mais vous perdez l’ergonomie du Builder (panneau d’options, icône, catégorie, prévisualisation cohérente). Et si vous modifiez le HTML/CSS du shortcode, vous avez vite des divergences entre pages.

À la fin, vous saurez créer un élément Fusion Builder custom sous forme de plugin (recommandé), avec :

  • un composant visible dans Avada Builder, avec icône, libellés et paramètres,
  • un rendu côté front sécurisé (sanitization + escaping),
  • des assets CSS/JS chargés proprement,
  • une base maintenable pour vos futurs éléments.

Résumé rapide

  • On crée un plugin WordPress (WP 6.9.4+, PHP 8.1+) qui enregistre un élément Avada Builder via les hooks fournis par Avada.
  • On déclare les paramètres (textes, liens, choix, couleurs) et leurs valeurs par défaut.
  • On génère le HTML avec échappement strict et on sécurise les entrées (notamment URL, attributs, classes).
  • On charge le CSS/JS uniquement quand l’élément est réellement présent (approche progressive : simple puis optimisée).
  • On ajoute une variante “dynamique” (ex. récupérer l’auteur courant, un champ personnalisé) sans casser l’UI.

Quand utiliser cette solution

  • Vous livrez un site Avada et vous voulez des composants réutilisables “propres”, pas une collection de shortcodes disparates.
  • Vous avez des contenus répétitifs avec une charte stricte (CTA, encarts FAQ, cartes services, témoignages).
  • Vous devez déléguer l’édition à des non-devs : ils doivent rester dans le Builder, sans toucher au HTML.
  • Vous voulez versionner votre composant (Git) et le déployer sur plusieurs sites.

Quand ne PAS utiliser cette solution

  • Le site n’utilise pas Avada Builder (ou prévoit de migrer vers Gutenberg/Blocks dans 3 mois). Dans ce cas, partez sur un bloc (block.json) natif WordPress.
  • Votre composant est ultra simple et ponctuel (un bouton stylé unique). Un élément natif Avada ou un style global suffit.
  • Vous cherchez un composant “data-driven” complexe (listing filtrable, requêtes AJAX, etc.). Vous pouvez le faire, mais prévoyez une vraie architecture (API REST, cache objet, endpoints) au lieu d’un simple élément.

Prérequis / avant de commencer

Je pars du principe que vous êtes sur WordPress 6.9.4 (avril 2026) et PHP 8.1+. Si vous testez ce code sur une vieille stack, vous allez rencontrer des erreurs (types, fonctions, warnings).

  • Environnement : un site de staging ou local (Local, DevKinsta, Docker). Évitez la prod pour le premier test.
  • Sauvegarde : base + fichiers. Sur Avada, une erreur PHP peut bloquer l’admin si vous activez un plugin cassé.
  • Avada : thème Avada + Avada Builder (Fusion Builder) à jour.
  • Outils : accès FTP/SSH, logs PHP, et idéalement WP-CLI.

Pour les références WordPress :

Note Avada : l’API exacte d’Avada Builder évolue. Le code ci-dessous suit le modèle “Fusion Builder Element” utilisé par Avada depuis plusieurs années (enregistrement via hooks Avada/Fusion, mapping des paramètres, rendu via shortcode/renderer). Si votre version Avada a renommé un hook, utilisez la section dépannage pour repérer le bon point d’entrée.

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

La version que je retrouve le plus souvent : un shortcode dans functions.php (thème enfant), qui concatène du HTML avec des attributs non échappés.

<?php
// ⚠️ Exemple volontairement mauvais : ne copiez pas ça.
add_shortcode('promo_box', function($atts) {
    $atts = shortcode_atts([
        'title' => '',
        'url'   => '',
        'color' => '#ff0000',
    ], $atts);

    // Problèmes : pas d'escaping, URL non validée, injection possible via title/url.
    return '<div class="promo" style="border-color:' . $atts['color'] . '">'
        . '<a href="' . $atts['url'] . '">'
        . '<strong>' . $atts['title'] . '</strong>'
        . '</a>'
        . '</div>';
});

Pourquoi c’est une mauvaise base :

  • Sécurité : si un éditeur peut insérer le shortcode, il peut injecter des attributs (XSS) via title/url/couleur.
  • Maintenance : functions.php n’est pas un “produit” versionné proprement. Un changement de thème, et vous perdez le composant.
  • Ergonomie : pas d’UI Avada Builder (paramètres, icône, catégorie). Les utilisateurs doivent mémoriser des attributs.
  • Performance : souvent on enqueue CSS/JS partout, “au cas où”.

La bonne approche — tutoriel pas à pas

Étape 1 — Créer un plugin dédié (recommandé)

Créez un dossier :

wp-content/plugins/bpcab-avada-elements/

Puis ces fichiers :

  • bpcab-avada-elements.php (bootstrap du plugin)
  • includes/class-bpcab-promo-box.php (élément Avada)
  • assets/css/promo-box.css
  • assets/js/promo-box.js (optionnel)

Étape 2 — Charger votre code seulement si Avada Builder est présent

Le piège classique : appeler une classe Fusion/Avada avant qu’Avada Builder ait chargé son framework. Résultat : fatal error “Class not found”.

Je préfère un chargement conditionnel sur plugins_loaded + une vérification simple (classe/fonction). Avada expose généralement des classes Fusion ; si ce n’est pas le cas chez vous, adaptez le test.

Étape 3 — Déclarer l’élément (paramètres + rendu)

On crée un élément “Promo Box” avec :

  • titre, texte, URL, ouverture dans un nouvel onglet,
  • style (variante), couleur d’accent, icône (classe),
  • option “nofollow/sponsored” (SEO),
  • classe CSS et id HTML.

Étape 4 — Sécuriser : sanitization et escaping

Deux couches :

  • Sanitization des attributs (quand ils entrent) : URL via esc_url_raw(), classes via sanitize_html_class(), booléens normalisés.
  • Escaping à la sortie : esc_html(), esc_attr(), esc_url().

Dans mon expérience, beaucoup de snippets “Avada element” sur le web oublient l’escaping, parce que “c’est du builder donc c’est safe”. Non : un compte éditeur compromis, un import de layout, et vous avez une surface XSS.

Étape 5 — Charger les assets proprement

Version simple : enqueuer le CSS sur le front pour tout le site. Ça marche, mais c’est inutilement global.

Version avancée : n’enqueue que si le shortcode/élément est présent dans le contenu (détection via has_shortcode() sur le contenu principal). Je vous donne les deux.

Code complet

1) Fichier principal du plugin : bpcab-avada-elements.php

<?php
/**
 * Plugin Name: BPCAB – Éléments Avada Builder (Custom)
 * Description: Ajoute un élément Avada (Fusion Builder) "Promo Box" avec options, rendu sécurisé et assets dédiés.
 * Version: 1.0.0
 * Author: BPCAB
 * Requires at least: 6.9
 * Requires PHP: 8.1
 * License: GPLv2 or later
 */

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

define( 'BPCAB_AVADA_ELEMENTS_VERSION', '1.0.0' );
define( 'BPCAB_AVADA_ELEMENTS_DIR', plugin_dir_path( __FILE__ ) );
define( 'BPCAB_AVADA_ELEMENTS_URL', plugin_dir_url( __FILE__ ) );

/**
 * Charge les éléments custom uniquement si Avada Builder (Fusion) est disponible.
 */
add_action( 'plugins_loaded', function() {

	// Test pragmatique : Avada Builder charge généralement des classes Fusion_*.
	// Adaptez si votre version expose un autre marqueur.
	$avada_present = class_exists( 'FusionBuilder' ) || class_exists( 'Fusion_Element' ) || function_exists( 'fusion_builder_map' );

	if ( ! $avada_present ) {
		// On ne fait rien si Avada Builder n'est pas actif.
		return;
	}

	require_once BPCAB_AVADA_ELEMENTS_DIR . 'includes/class-bpcab-promo-box.php';

	// Instanciation (enregistrement hooks + shortcode/élément).
	BPCAB_Promo_Box::instance();
}, 20 );

/**
 * Enqueue CSS/JS (version simple : global front).
 * Variante optimisée fournie plus bas dans l’article.
 */
add_action( 'wp_enqueue_scripts', function() {
	wp_register_style(
		'bpcab-promo-box',
		BPCAB_AVADA_ELEMENTS_URL . 'assets/css/promo-box.css',
		[],
		BPCAB_AVADA_ELEMENTS_VERSION
	);

	wp_register_script(
		'bpcab-promo-box',
		BPCAB_AVADA_ELEMENTS_URL . 'assets/js/promo-box.js',
		[],
		BPCAB_AVADA_ELEMENTS_VERSION,
		true
	);

	// Simple : on charge uniquement le CSS (souvent suffisant).
	wp_enqueue_style( 'bpcab-promo-box' );

	// JS optionnel : ne l'enqueuez que si vous en avez besoin.
	// wp_enqueue_script( 'bpcab-promo-box' );
}, 20 );

2) Classe de l’élément : includes/class-bpcab-promo-box.php

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

final class BPCAB_Promo_Box {

	private static ?self $instance = null;

	/**
	 * Nom du shortcode rendu par l'élément.
	 * Avada Builder s'appuie très souvent sur des shortcodes pour le rendu.
	 */
	private string $shortcode = 'bpcab_promo_box';

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

	private function __construct() {
		// Enregistre le shortcode (rendu front).
		add_shortcode( $this->shortcode, [ $this, 'render_shortcode' ] );

		/**
		 * Enregistre l'élément côté Builder.
		 * Selon les versions Avada, le hook peut varier.
		 * - fusion_builder_before_init est courant
		 * - fusion_builder_map ou fusion_builder_init existent aussi selon branches
		 */
		add_action( 'fusion_builder_before_init', [ $this, 'register_fusion_element' ], 20 );

		// Fallback si le hook principal n'existe pas dans votre version.
		add_action( 'init', [ $this, 'register_fusion_element_fallback' ], 30 );
	}

	/**
	 * Enregistrement principal (si Avada déclenche bien fusion_builder_before_init).
	 */
	public function register_fusion_element(): void {
		$this->map_element();
	}

	/**
	 * Fallback : on map l'élément sur init si fusion_builder_before_init n'a pas été appelé.
	 * On évite les doubles enregistrements via un flag statique.
	 */
	public function register_fusion_element_fallback(): void {
		static $done = false;
		if ( $done ) {
			return;
		}

		// Si la fonction de mapping n'existe pas, inutile d'aller plus loin.
		if ( ! function_exists( 'fusion_builder_map' ) ) {
			return;
		}

		$done = true;
		$this->map_element();
	}

	/**
	 * Déclare l'élément dans Avada Builder.
	 */
	private function map_element(): void {
		if ( ! function_exists( 'fusion_builder_map' ) ) {
			return;
		}

		fusion_builder_map(
			[
				'name'        => esc_attr__( 'Promo Box (BPCAB)', 'bpcab-avada' ),
				'shortcode'   => $this->shortcode,
				'icon'        => 'fusiona-bullhorn', // Icône Avada (classe). Ajustez selon votre set.
				'category'    => esc_attr__( 'BPCAB', 'bpcab-avada' ),
				'description' => esc_attr__( 'Encart promotionnel avec bouton et options SEO.', 'bpcab-avada' ),
				'params'      => $this->params_definition(),
			]
		);
	}

	/**
	 * Définition des paramètres affichés dans l'UI Avada Builder.
	 * La structure exacte dépend d'Avada, mais ce format est largement compatible.
	 */
	private function params_definition(): array {
		return [
			[
				'type'        => 'textfield',
				'heading'     => esc_attr__( 'Titre', 'bpcab-avada' ),
				'param_name'  => 'title',
				'value'       => '',
				'description' => esc_attr__( 'Titre principal de l’encart.', 'bpcab-avada' ),
			],
			[
				'type'        => 'textarea',
				'heading'     => esc_attr__( 'Texte', 'bpcab-avada' ),
				'param_name'  => 'text',
				'value'       => '',
				'description' => esc_attr__( 'Texte descriptif (une ou deux phrases).', 'bpcab-avada' ),
			],
			[
				'type'        => 'link_selector',
				'heading'     => esc_attr__( 'Lien (URL)', 'bpcab-avada' ),
				'param_name'  => 'url',
				'value'       => '',
				'description' => esc_attr__( 'URL du bouton / lien principal.', 'bpcab-avada' ),
			],
			[
				'type'        => 'textfield',
				'heading'     => esc_attr__( 'Texte du bouton', 'bpcab-avada' ),
				'param_name'  => 'button_text',
				'value'       => esc_attr__( 'En savoir plus', 'bpcab-avada' ),
			],
			[
				'type'        => 'radio_button_set',
				'heading'     => esc_attr__( 'Ouvrir dans un nouvel onglet', 'bpcab-avada' ),
				'param_name'  => 'target_blank',
				'value'       => [
					'no'  => esc_attr__( 'Non', 'bpcab-avada' ),
					'yes' => esc_attr__( 'Oui', 'bpcab-avada' ),
				],
				'default'     => 'no',
			],
			[
				'type'        => 'radio_button_set',
				'heading'     => esc_attr__( 'Relation SEO', 'bpcab-avada' ),
				'param_name'  => 'rel',
				'value'       => [
					''           => esc_attr__( 'Aucune', 'bpcab-avada' ),
					'nofollow'   => 'nofollow',
					'sponsored'  => 'sponsored',
					'ugc'        => 'ugc',
				],
				'default'     => '',
				'description' => esc_attr__( 'Ajoute rel="nofollow|sponsored|ugc" sur le lien.', 'bpcab-avada' ),
			],
			[
				'type'        => 'select',
				'heading'     => esc_attr__( 'Style', 'bpcab-avada' ),
				'param_name'  => 'variant',
				'value'       => [
					'soft'   => esc_attr__( 'Soft (fond léger)', 'bpcab-avada' ),
					'border' => esc_attr__( 'Border (bordure)', 'bpcab-avada' ),
					'solid'  => esc_attr__( 'Solid (fond plein)', 'bpcab-avada' ),
				],
				'default'     => 'soft',
			],
			[
				'type'        => 'colorpicker',
				'heading'     => esc_attr__( 'Couleur d’accent', 'bpcab-avada' ),
				'param_name'  => 'accent_color',
				'value'       => '#2271b1',
				'description' => esc_attr__( 'Couleur utilisée pour la bordure / bouton selon le style.', 'bpcab-avada' ),
			],
			[
				'type'        => 'textfield',
				'heading'     => esc_attr__( 'Icône (classe CSS)', 'bpcab-avada' ),
				'param_name'  => 'icon_class',
				'value'       => 'fusiona-bullhorn',
				'description' => esc_attr__( 'Ex: fusiona-bullhorn (ou une classe Font Awesome si chargée).', 'bpcab-avada' ),
			],
			[
				'type'        => 'textfield',
				'heading'     => esc_attr__( 'HTML ID', 'bpcab-avada' ),
				'param_name'  => 'id',
				'value'       => '',
			],
			[
				'type'        => 'textfield',
				'heading'     => esc_attr__( 'Classe CSS', 'bpcab-avada' ),
				'param_name'  => 'class',
				'value'       => '',
			],
		];
	}

	/**
	 * Rendu du shortcode (front).
	 * @param array|string $atts
	 * @param string|null $content
	 */
	public function render_shortcode( $atts, $content = null ): string {

		$defaults = [
			'title'        => '',
			'text'         => '',
			'url'          => '',
			'button_text'  => __( 'En savoir plus', 'bpcab-avada' ),
			'target_blank' => 'no',
			'rel'          => '',
			'variant'      => 'soft',
			'accent_color' => '#2271b1',
			'icon_class'   => 'fusiona-bullhorn',
			'id'           => '',
			'class'        => '',
		];

		$atts = shortcode_atts( $defaults, is_array( $atts ) ? $atts : [], $this->shortcode );

		// Sanitization (entrée).
		$title        = sanitize_text_field( (string) $atts['title'] );
		$text         = sanitize_textarea_field( (string) $atts['text'] );
		$url          = esc_url_raw( (string) $atts['url'] );
		$button_text  = sanitize_text_field( (string) $atts['button_text'] );
		$target_blank = ( (string) $atts['target_blank'] === 'yes' ) ? 'yes' : 'no';

		$allowed_rel = [ '', 'nofollow', 'sponsored', 'ugc' ];
		$rel         = in_array( (string) $atts['rel'], $allowed_rel, true ) ? (string) $atts['rel'] : '';

		$allowed_variant = [ 'soft', 'border', 'solid' ];
		$variant         = in_array( (string) $atts['variant'], $allowed_variant, true ) ? (string) $atts['variant'] : 'soft';

		// Couleur : on autorise uniquement les formats hex simples.
		$accent_color = (string) $atts['accent_color'];
		if ( ! preg_match( '/^#([A-Fa-f0-9]{3}){1,2}$/', $accent_color ) ) {
			$accent_color = '#2271b1';
		}

		// Classe icône : on limite à des caractères safe (lettres, chiffres, tirets, underscores, espaces).
		$icon_class = preg_replace( '/[^A-Za-z0-9-_ ]/', '', (string) $atts['icon_class'] );
		$icon_class = trim( $icon_class );

		// id + class : sécurisation.
		$id_attr = '';
		if ( ! empty( $atts['id'] ) ) {
			// ID HTML : on garde une version "slug".
			$id_sanitized = sanitize_title( (string) $atts['id'] );
			if ( $id_sanitized ) {
				$id_attr = $id_sanitized;
			}
		}

		$extra_classes = [];
		if ( ! empty( $atts['class'] ) ) {
			// Support multi-classes : on split et sanitize.
			$pieces = preg_split( '/s+/', (string) $atts['class'] );
			if ( is_array( $pieces ) ) {
				foreach ( $pieces as $piece ) {
					$piece = sanitize_html_class( $piece );
					if ( $piece ) {
						$extra_classes[] = $piece;
					}
				}
			}
		}

		$classes = array_merge(
			[ 'bpcab-promo-box', 'bpcab-promo-box--' . $variant ],
			$extra_classes
		);

		// Attributs lien.
		$link_attrs = [];
		if ( $url ) {
			$link_attrs['href'] = $url;
		}
		if ( $target_blank === 'yes' ) {
			$link_attrs['target'] = '_blank';
			// Sécurité : noopener/noreferrer pour éviter tabnabbing.
			$link_attrs['rel'] = 'noopener noreferrer' . ( $rel ? ' ' . $rel : '' );
		} elseif ( $rel ) {
			$link_attrs['rel'] = $rel;
		}

		// Style inline minimal (accent) : acceptable si limité et validé.
		$style = sprintf( '--bpcab-accent:%s;', $accent_color );

		ob_start();
		?>
		<div
			class="<?php echo esc_attr( implode( ' ', array_filter( $classes ) ) ); ?>"
			<?php if ( $id_attr ) : ?>id="<?php echo esc_attr( $id_attr ); ?>"<?php endif; ?>
			style="<?php echo esc_attr( $style ); ?>"
		>

			<div class="bpcab-promo-box__inner">

				<?php if ( $icon_class ) : ?>
					<span class="bpcab-promo-box__icon" aria-hidden="true">
						<span class="<?php echo esc_attr( $icon_class ); ?>"></span>
					</span>
				<?php endif; ?>

				<div class="bpcab-promo-box__content">
					<?php if ( $title ) : ?>
						<div class="bpcab-promo-box__title"><?php echo esc_html( $title ); ?></div>
					<?php endif; ?>

					<?php if ( $text ) : ?>
						<div class="bpcab-promo-box__text"><?php echo esc_html( $text ); ?></div>
					<?php endif; ?>

					<?php if ( $url ) : ?>
						<div class="bpcab-promo-box__actions">
							<a class="bpcab-promo-box__button"
								<?php
								foreach ( $link_attrs as $k => $v ) {
									printf( ' %s="%s"', esc_attr( $k ), esc_attr( $v ) );
								}
								?>
							>
								<?php echo esc_html( $button_text ); ?>
							</a>
						</div>
					<?php endif; ?>
				</div>

			</div>
		</div>
		<?php
		return (string) ob_get_clean();
	}
}

3) CSS : assets/css/promo-box.css

.bpcab-promo-box{
  --bpcab-accent:#2271b1;
  border-radius:12px;
  padding:18px;
  margin:0;
}

.bpcab-promo-box__inner{
  display:flex;
  gap:14px;
  align-items:flex-start;
}

.bpcab-promo-box__icon{
  flex:0 0 auto;
  width:38px;
  height:38px;
  border-radius:10px;
  background:color-mix(in srgb, var(--bpcab-accent) 12%, transparent);
  display:flex;
  align-items:center;
  justify-content:center;
}

.bpcab-promo-box__title{
  font-weight:700;
  line-height:1.25;
  margin:0 0 6px 0;
}

.bpcab-promo-box__text{
  opacity:.9;
  margin:0 0 12px 0;
}

.bpcab-promo-box__button{
  display:inline-block;
  padding:10px 14px;
  border-radius:10px;
  text-decoration:none;
  border:1px solid var(--bpcab-accent);
}

.bpcab-promo-box--soft{
  background:color-mix(in srgb, var(--bpcab-accent) 7%, transparent);
  border:1px solid color-mix(in srgb, var(--bpcab-accent) 18%, transparent);
}

.bpcab-promo-box--border{
  background:transparent;
  border:2px solid color-mix(in srgb, var(--bpcab-accent) 55%, #fff);
}

.bpcab-promo-box--solid{
  background:var(--bpcab-accent);
  border:1px solid var(--bpcab-accent);
  color:#fff;
}

.bpcab-promo-box--solid .bpcab-promo-box__button{
  background:#fff;
  color:#111;
  border-color:#fff;
}

4) JS (optionnel) : assets/js/promo-box.js

(function(){
  // Exemple minimal : rien par défaut.
  // Ajoutez ici une interaction si vous en avez besoin (tracking, animation, etc.).
})();

Explication du code

Pourquoi un plugin (et pas functions.php)

Le plugin isole votre composant. Vous pouvez le versionner, le déployer, et le désactiver sans toucher au thème. Sur Avada, c’est particulièrement utile parce que beaucoup de sites ont un thème enfant “fourre-tout” qui finit par devenir ingérable.

Chargement conditionnel Avada

Le bootstrap teste la présence d’Avada Builder via class_exists() / function_exists(). L’objectif n’est pas d’être “parfait”, mais d’éviter le fatal error si le plugin est activé sur un site sans Avada.

J’ai souvent vu ce bug sur des multisites : un MU-plugin force l’activation réseau, et un sous-site n’a pas Avada. Sans garde-fou, tout le réseau tombe.

Mapping de l’élément

La méthode map_element() appelle fusion_builder_map() avec :

  • name : libellé dans le Builder
  • shortcode : le shortcode utilisé pour le rendu
  • params : définition des champs UI

Le fallback init évite un cas réel : sur certains setups, le hook Avada “before init” ne se déclenche pas (ou se déclenche avant votre plugin). Le flag $done évite le double mapping.

Sanitization vs escaping

Le rendu applique d’abord une sanitization (texte, textarea, URL brute, classes). Ensuite, chaque sortie HTML est échappée au bon endroit :

  • esc_html() pour le texte visible
  • esc_attr() pour les attributs HTML
  • esc_url() / esc_url_raw() pour les URL

Sources officielles : Sanitizing et Escaping.

rel / target et sécurité

Si target="_blank" est activé, on force rel="noopener noreferrer". C’est un détail que beaucoup oublient, et ça ouvre la porte au tabnabbing. Ensuite on concatène éventuellement nofollow / sponsored / ugc.

Style inline minimal et contrôlé

Oui, on met un style inline… mais uniquement une variable CSS, et uniquement après validation hex. C’est un compromis propre : vous évitez de générer une feuille CSS par instance, tout en gardant la personnalisation.

Variantes et cas d’usage

Variante 1 — Enqueue des assets uniquement si l’élément est utilisé

Si vous avez 40 pages et que seulement 3 utilisent l’élément, charger le CSS partout est inutile. Voici une version “raisonnable” : on enregistre les assets, puis on les enqueue seulement si le contenu principal contient le shortcode.

Ajoutez ceci dans bpcab-avada-elements.php à la place de l’enqueue global (ou en complément en désactivant le global).

<?php
add_action( 'wp_enqueue_scripts', function() {

	wp_register_style(
		'bpcab-promo-box',
		BPCAB_AVADA_ELEMENTS_URL . 'assets/css/promo-box.css',
		[],
		BPCAB_AVADA_ELEMENTS_VERSION
	);

	// Détection simple sur le contenu du post principal.
	if ( is_singular() ) {
		$post = get_post();
		if ( $post instanceof WP_Post && has_shortcode( (string) $post->post_content, 'bpcab_promo_box' ) ) {
			wp_enqueue_style( 'bpcab-promo-box' );
		}
	}
}, 20 );

Edge case : si l’élément est injecté via un template Avada (ou un global layout) et pas dans post_content, has_shortcode() ne le verra pas. Dans ce cas, revenez à l’enqueue global, ou implémentez une détection plus large (ex. filtrer le contenu final, ou utiliser un flag dans le rendu du shortcode).

Variante 2 — Ajouter un “mode dynamique” (auteur courant)

Cas fréquent sur des blogs : un encart “Suivre l’auteur” en bas d’article. Vous pouvez ajouter un paramètre dynamic_author et, si activé, remplacer le titre/texte par des données auteur.

Dans params_definition(), ajoutez :

[
  'type'       => 'radio_button_set',
  'heading'    => esc_attr__( 'Mode auteur (automatique)', 'bpcab-avada' ),
  'param_name' => 'dynamic_author',
  'value'      => [
    'no'  => esc_attr__( 'Non', 'bpcab-avada' ),
    'yes' => esc_attr__( 'Oui', 'bpcab-avada' ),
  ],
  'default'    => 'no',
],

Puis dans render_shortcode() (après shortcode_atts) :

$dynamic_author = ( (string) ( $atts['dynamic_author'] ?? 'no' ) === 'yes' );

if ( $dynamic_author && is_singular() ) {
	$post = get_post();
	if ( $post instanceof WP_Post ) {
		$author_id = (int) $post->post_author;
		if ( $author_id > 0 ) {
			$display = get_the_author_meta( 'display_name', $author_id );
			$title   = $display ? sprintf( 'À propos de %s', $display ) : $title;

			$bio = get_the_author_meta( 'description', $author_id );
			if ( $bio ) {
				$text = wp_trim_words( wp_strip_all_tags( $bio ), 26, '…' );
			}
		}
	}
}

Note : ici, on passe par wp_strip_all_tags() + wp_trim_words() pour éviter d’afficher des bios avec HTML non maîtrisé.

Variante 3 — Rendu “sans bouton” si URL vide

Le code le fait déjà : si l’URL est vide, on n’affiche pas le bouton. C’est un comportement que je préfère à “bouton désactivé”, parce que ça évite des liens morts (et des audits SEO qui râlent).

Compatibilité Divi 5 / Elementor / Avada

Avada (Fusion Builder)

C’est le cœur du tutoriel : l’élément apparaît dans Avada Builder et se configure via les params. Si vous utilisez des “Global Elements” Avada, ce composant se prête bien à la centralisation (un seul endroit à éditer).

Elementor

Elementor ne saura pas “lire” votre élément Avada en tant que widget natif. En revanche :

  • le shortcode [bpcab_promo_box] reste utilisable dans un widget “Shortcode” Elementor,
  • le CSS/JS continuera de fonctionner si enqueued côté front.

Si vous devez supporter Elementor proprement, créez un widget Elementor dédié (PHP + controls). C’est un autre chantier, mais votre logique de rendu (sanitization + HTML) peut être réutilisée.

Divi 5

Même logique : Divi 5 ne convertit pas un élément Avada. Mais le shortcode est portable (module Code/Shortcode). Si vous avez besoin d’une intégration Divi 5 “native”, vous devrez créer un module Divi (API Divi 5) et réutiliser le rendu.

Dans les migrations Avada → Divi/Elementor que j’ai accompagnées, le fait d’avoir un shortcode propre derrière l’élément Avada est un vrai avantage : vous remplacez l’UI, pas le contenu.

Vérifications après mise en place

  1. Activez le plugin dans Extensions.
  2. Ouvrez une page avec Avada Builder : cherchez la catégorie BPCAB ou l’élément Promo Box (BPCAB).
  3. Insérez l’élément, remplissez titre/texte/URL, publiez.
  4. Vérifiez côté front :
    • le HTML est présent,
    • le style s’applique,
    • le lien a bien rel attendu (et noopener si nouvel onglet).
  5. Testez un cas “hostile” : mettez des guillemets, chevrons, caractères spéciaux dans le titre. Rien ne doit casser, rien ne doit être interprété comme HTML.

Si ça ne marche pas

Checklist rapide

  • Vous avez copié le code au bon endroit (plugin, pas functions.php) ?
  • Votre PHP est bien en 8.1+ ? (beaucoup de serveurs “hébergements mutualisés” traînent encore)
  • Avada Builder est actif et à jour ?
  • Vous n’avez pas une erreur fatale dans les logs ?
  • Vous avez vidé le cache (plugin cache + cache serveur + cache navigateur) ? J’ai vu des éléments “invisibles” juste à cause d’un cache agressif.

Tableau de diagnostic

Symptôme Cause probable Vérification Solution
L’élément n’apparaît pas dans Avada Builder Hook Avada différent / mapping non exécuté Activez WP_DEBUG, cherchez si fusion_builder_map existe au runtime Utilisez le fallback init, ajustez le hook (fusion_builder_init selon votre version)
Erreur “Call to undefined function fusion_builder_map()” Avada Builder non chargé au moment du mapping Logs PHP, stacktrace Montez la priorité, ou mappez sur le hook Avada correct, et gardez le function_exists
Le plugin casse le site (fatal error) à l’activation Syntax error (point-virgule/parenthèse), ou PHP trop ancien Logs, écran blanc, email “fatal error” WP Corrigez la syntaxe, vérifiez PHP 8.1+, désactivez via FTP si besoin
Le style ne s’applique pas CSS non enqueued, cache, ou conflit de spécificité Avada Onglet Network/Styles du navigateur Vérifiez wp_enqueue_style, videz cache, augmentez la spécificité CSS
Le bouton ouvre un nouvel onglet mais sans rel noopener Attributs link mal assemblés Inspecteur HTML Gardez la logique target_blank + concat rel telle quelle

Erreurs réalistes que je vois souvent

  • Copier le code dans le mauvais fichier : mettre la classe dans functions.php et oublier le require_once. Résultat : rien n’est instancié.
  • Oublier un point-virgule dans la déclaration du tableau params. PHP ne pardonne pas, et WordPress peut devenir inaccessible si vous n’avez pas le recovery mode.
  • Hook inadapté : mapper l’élément sur init sans vérifier si Avada Builder a déjà chargé ses fonctions.
  • Cache : Avada + plugin de cache + CDN. Vous modifiez le CSS, rien ne change. Force refresh + purge.
  • Code d’un vieux tutoriel : certains exemples utilisent des classes Fusion qui ont changé. Sur WP 6.9.4, ça se traduit par des “Class not found”. Gardez des tests class_exists.

Pièges et erreurs courantes

Erreur Cause Solution
“Parse error: syntax error, unexpected …” Virgule manquante dans un tableau PHP, accolade non fermée Reformatez le fichier, utilisez un IDE, comparez avec le code complet
Élément visible mais options vides / non sauvegardées param_name différent des clés attendues dans shortcode_atts Alignez strictement les noms (ex. accent_color)
CSS chargé mais écrasé par Avada Spécificité plus faible, ou styles globaux Avada Augmentez la spécificité (ex. préfixer par .fusion-body) ou utilisez des variables CSS
Le lien accepte des valeurs étranges (javascript:…) URL non nettoyée (pas de esc_url_raw) Gardez esc_url_raw à l’entrée et esc_url à la sortie
Double enregistrement de l’élément (doublon dans le Builder) Mapping exécuté sur deux hooks Gardez un flag $done (comme dans le fallback) ou supprimez un hook
“Undefined function has_shortcode()” (rare) Code exécuté trop tôt (avant chargement complet) Enqueue sur wp_enqueue_scripts ou plus tard, pas sur plugins_loaded

Conseils sécurité, performance et maintenance

  • Permissions : si vous autorisez des rôles non-admin à utiliser le Builder, considérez le shortcode comme une surface d’entrée. L’escaping strict est non négociable.
  • Évitez le HTML libre dans les params (type “textarea_html”) sauf si vous passez par wp_kses() avec une whitelist stricte. Sinon, vous créez un XSS stocké.
  • Assets : commencez simple, puis optimisez (détection du shortcode). N’allez pas trop loin si Avada injecte le contenu via des templates globaux : vous risquez de ne pas charger le CSS quand il faut.
  • Compatibilité future : gardez le rendu dans une méthode isolée. Si un jour vous migrez vers un bloc Gutenberg, vous réutiliserez la logique de sanitization + templating.
  • Debug propre : activez WP_DEBUG en staging, loggez dans wp-content/debug.log. Référence : Debug WordPress.
  • Versioning : incrémentez BPCAB_AVADA_ELEMENTS_VERSION pour invalider les caches CSS/JS.

Ressources

FAQ

Est-ce que ce composant est “vraiment” un élément Avada, ou juste un shortcode ?

C’est les deux, et c’est volontaire. Avada Builder mappe l’élément vers un shortcode pour le rendu, ce qui le rend portable (Elementor/Divi peuvent encore l’exécuter via un module shortcode). L’UI Avada reste native.

Où placer ce code si je ne veux pas créer un plugin ?

Techniquement, dans le thème enfant. En pratique, je déconseille : vous perdez la portabilité et vous risquez de casser le site lors d’une mise à jour/maintenance du thème enfant. Si vous insistez, mettez-le dans functions.php + un dossier /includes, mais gardez le chargement conditionnel.

Pourquoi utiliser sanitize_textarea_field au lieu de laisser du HTML dans “Texte” ?

Parce que c’est le chemin le plus sûr. Si vous autorisez du HTML, utilisez wp_kses() avec une whitelist. Sinon, un import de layout ou un compte compromis peut injecter du JS.

Je veux permettre des retours à la ligne dans le texte. Comment faire ?

Deux options :

  • Transformez les sauts de ligne en <br> avec nl2br( esc_html( $text ) ) (attention : échappez avant).
  • Ou enveloppez dans <p> via wpautop( esc_html( $text ) ) (ça reste du texte, pas du HTML arbitraire).

Pourquoi ne pas utiliser directement un template PHP séparé pour le HTML ?

Vous pouvez, et c’est souvent plus propre à grande échelle. Pour un seul élément, garder le rendu dans la classe évite de multiplier les fichiers. Dès que vous avez 3–4 éléments, je passe généralement à des templates (et éventuellement à un mini moteur de rendu).

Comment ajouter un champ “Image” dans les paramètres ?

Avada propose généralement un type de champ pour médias (selon version). Le principe reste : stocker un ID d’attachement, puis générer l’URL via wp_get_attachment_image() ou wp_get_attachment_image_url() et échapper correctement.

Mon CSS ne se charge pas dans l’éditeur visuel Avada, seulement côté front

Selon la configuration Avada, l’éditeur peut charger un set CSS distinct. Si vous devez absolument voir le style dans le builder, ajoutez aussi un enqueue côté admin/builder (Avada a parfois des hooks dédiés). Commencez par vérifier si wp_enqueue_scripts est exécuté dans votre mode d’édition.

Est-ce compatible multisite ?

Oui, si Avada Builder est activé sur les sites qui utilisent l’élément. Gardez le chargement conditionnel : c’est lui qui évite les fatal errors sur des sous-sites sans Avada.

Je vois un doublon de l’élément dans la liste

Ça arrive si le mapping est appelé deux fois (hook Avada + fallback). Supprimez le fallback si votre hook Avada fonctionne, ou gardez le flag (déjà présent) et vérifiez que vous n’instanciez pas la classe deux fois.

Comment tester proprement ce code avant livraison ?

  • Testez sur une page vierge, puis sur une page complexe Avada (containers/columns).
  • Testez avec un rôle Éditeur (si votre site le permet) pour vérifier qu’aucune entrée ne casse le HTML.
  • Ajoutez des valeurs extrêmes : texte long, URL avec paramètres, classes multiples.
  • Contrôlez le HTML généré (validité, attributs, rel/target).
  • Mesurez : Lighthouse/Perf pour vérifier que vous n’avez pas ajouté 200 KB de CSS global inutile.