Se il tuo cliente ti chiede “un blocco Elementor, che visualizza un riquadro autore con un link ai suoi ultimi articoli, ti dà due possibilità: creare uno shortcode, oppure créer Un vero widget Elementor, correttamente configurabile e manutenibile. Lo shortcode funziona... finché non arriva il giorno in cui devi aggiungere un controllo di stile, gestire un fallback o evitare di caricare CSS ovunque.

Il problema / Il bisogno

Desideri un widget Elementor personalizzato e riutilizzabile che si integri nell'editor come un qualsiasi widget nativo. E, soprattutto, lo desideri configurabile (contenuto e stili), sicuro (sanificazione/escaping) e che non influisca sulle prestazioni del sito.

Questa guida è pensata per utenti di livello intermedio (che abbiano familiarità con PHP e gli hook) che lavorano con WordPress 6.9.4 (aprile 2026) e PHP 8.1+. Al termine, saprai:

  • Crea un mini-plug-in che registra un widget tramite l'API Widget di Elementor.
  • Aggiungi controlli (testo, URL, selettore utente, numero di articoli, interruttore opzioni).
  • Genera un rendering HTML sicuro (con escape) e robusto (con fallback).
  • Carica i file CSS/JS solo quando il widget viene utilizzato.

Riassunto veloce

  • Creiamo un plugin “mu” o classico che si collega a elementor/widgets/register.
  • Definiamo una classe widget che estende ElementorWidget_Base.
  • Aggiungiamo controlli tramite Controls_Manager (contenuto + stile).
  • Eseguiamo il rendering del widget con render() (davanti) e content_template() (Anteprima dell'editor, facoltativa).
  • Salviamo le risorse (CSS/JS) e le carichiamo tramite get_style_depends()/get_script_depends().

Quando utilizzare questa soluzione

  • È presente un blocco specifico per il progetto (ad esempio, "Inserimento autore", "CTA home", "Prodotti in evidenza") che i tuoi editor devono essere in grado di configurare visivamente.
  • È necessario rendere accessibili i controlli di stile di Elementor (tipografia, colori, spaziatura) senza dover scrivere un insieme complesso di classi CSS.
  • È meglio evitare shortcode "magici" che compromettono il layout quando si cambiano temi o strumenti di creazione siti web.
  • Se gestisci più siti: un widget confezionato come plugin è più facile da versionare e distribuire.

Quando NON utilizzare questa soluzione

  • Se desideri inserire solo un piccolo frammento di codice HTML statico: utilizza un widget "HTML" o un template di Elementor.
  • Hai bisogno di un rendering lato server completamente dinamico, ma senza un'interfaccia utente complessa: uno shortcode potrebbe essere sufficiente (e può essere utilizzato in Elementor tramite il widget "Shortcode").
  • Se stai cercando un componente cross-builder riutilizzabile, dai la priorità a Blocco Gutenberg (Editor a blocchi) e/o un modello. Elementor è un ecosistema specifico.
  • Non hai il controllo sul codice (sito del cliente bloccato): un plugin per "snippet" può essere d'aiuto, ma raramente garantisce un caricamento pulito di classi/risorse.

Prerequisiti / prima di iniziare

Prima di modificare il codice:

  • Lavora in un ambiente di staging/locale (LocalWP, DevKinsta, Docker...).
  • Eseguire il backup del database e dei file (almeno wp-content).
  • Verifica le versioni: WordPress 6.9.4, PHP 8.1+, Elementor aggiornato.
  • permettere WP_DEBUG et WP_DEBUG_LOG in fase di staging per vedere gli errori.

Promemoria utili:

Precauzione di sicurezza: un widget Elementor può visualizzare dati dal database (utenti, post). Se non si gestisce correttamente l'output, si aprono le porte a vulnerabilità XSS persistenti. Ho già visto questo scenario su siti multi-autore in cui un "display_name" filtrato in modo inadeguato è finito per essere iniettato in un attributo HTML.

L'approccio ingenuo (e perché evitarlo)

Ciò che vedo spesso: uno shortcode che recupera le opzioni tramite $_GET o attributi non filtrati, quindi stampa HTML grezzo. Esempio tipico (non utilizzare):

<?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>';
});

Problemi concreti:

  • sicurezza : $atts['title'] et $user->display_name se ne vanno senza esc_html().
  • Perf : nessuna cache, nessun limite rigido e si rischia di caricare richieste ripetute in una pagina Elementor.
  • UX In Elementor, l'editor non riconosce i controlli nativi (nessuna tipografia/colore senza CSS personalizzato).
  • Manutenzione : alla fine ti ritrovi con 12 shortcode, ognuno con la propria logica e CSS complessivo.

L'approccio corretto: tutorial passo passo

Passaggio 1 — Crea un plugin minimale

Crea una cartella: wp-content/plugins/bpcab-elementor-widgets

Crea il file principale: wp-content/plugins/bpcab-elementor-widgets/bpcab-elementor-widgets.php

Passaggio 2: verificare che Elementor sia caricato (e al momento giusto)

La classica trappola: salvare il widget troppo presto (ad esempio, su init) e ottenere un errore del tipo Classe 'ElementorWidget_Base' non trovataCi colleghiamo agli hook dedicati di Elementor.

Passaggio 3: Dichiarare un widget "Pannello autore + Ultimi articoli"

Stiamo per programmare un widget che:

  • Consente di selezionare un utente (autore) tramite un controllo.
  • Mostra avatar + nome + biografia (facoltativa).
  • Elenco dei suoi articoli più recenti (numero configurabile).
  • Mostra le opzioni di stile (colori, tipografia, spaziatura).

Passaggio 4: carica CSS/JS solo se necessario.

Elementor ti consente di dichiarare le dipendenze tramite get_style_depends() et get_script_depends()Nella mia esperienza, questo è un grande vantaggio: si evita che un file CSS globale venga caricato su tutte le pagine.

Passaggio 5: Aggiungi i controlli di stile di Elementor

I "selettori" di Elementor verranno utilizzati per generare CSS specifico per il widget. Questo evita di dover scrivere decine di classi e riduce i conflitti con Avada/Divi.

Passaggio 6 — Rendering sicuro e robusto

Punti chiave:

  • Impostazioni di sanificazione: absint, sanitize_text_field, esc_url_raw se necessario.
  • Fuga dal lato dell'uscita: esc_html, esc_attr, esc_url.
  • Opzioni alternative: autore non trovato, nessun articolo, biografia vuota.

Codice completo

Copia e incolla così com'è. Questo plugin salva 1 widget Elementor. È volutamente compatto ma completo e pronto per essere testato.

1) File principale del 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 Widget

Creare: 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 del widget

Creare: 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 (facoltativo)

Creare: 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. */

Codice Spiegazione

Cosa succede dietro le quinte (versione semplificata)

Il plugin attende il caricamento di Elementor. Quindi, salva un widget. Elementor elenca questo widget nell'editor e, quando lo trascini su una pagina, Elementor:

  • visualizza i tuoi controlli (contenuto + stile),
  • memorizza le impostazioni nel JSON della pagina (post meta),
  • appelle render() per produrre l'HTML front-end.

Perché proprio questi ganci e non altri?

  • plugins_loaded È il momento giusto per verificare l'ambiente e la presenza di Elementor.
  • did_action('elementor/loaded') : evita di chiamare le classi di Elementor prima che vengano caricate automaticamente.
  • elementor/widgets/register : hook destinato alla registrazione dei widget. È questo che previene gli errori "Classe non trovata".
  • wp_register_style Prepariamo le risorse, ma non le colleghiamo a livello globale.

Sanificazione vs. fuga (gli errori che vedo più spesso)

Due regole:

  • sanificazione : quando si normalizza un valore (ad esempio: absint per un ID, sanitize_text_field (per un titolo).
  • Fuggire : quando stampi in HTML (ad esempio: esc_html, esc_url, esc_attr).

Nel widget, le impostazioni vengono sanificate al momento del rendering, quindi sistematicamente ripulite al momento della visualizzazione. Sì, Elementor memorizza già valori "puliti" nella maggior parte dei casi, ma non fateci troppo affidamento: un'importazione JSON, un copia-incolla o un plugin di terze parti possono inserire stringhe inaspettate.

Caricamento condizionale delle attività

get_style_depends() Restituisce un handle di stile salvato. Elementor caricherà questo stile solo se il widget è presente nella pagina. Questo è un semplice schema che evita l'utilizzo di CSS a livello di sito.

Richiesta degli articoli più recenti

Noi usiamo get_posts() insieme a:

  • no_found_rows : nessuna paginazione, quindi non c'è bisogno di contare.
  • ignore_sticky_posts : evita risultati sorprendenti.
  • numberposts limitato a 12: mantiene un widget “leggero”.

Possibile alternativa: WP_Query se hai bisogno di più controllo, ma qui get_posts() sufficiente e rimane leggibile.

Varianti e casi d'uso

Variante 1 — Selettore autore più intuitivo (elenco a discesa)

Il controllo "ID utente" è utile come esempio, ma i tuoi editori ti malediranno. Un'alternativa più semplice: crea un elenco di opzioni dagli utenti (fai attenzione con i siti con migliaia di utenti).

In register_controls()sostituire il controllo user_id da:

<?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(),
	]
);

Caso limite: su un sito con 50.000 account, questo controllo SELECT diventa inutilizzabile. In questo caso, passare a un controllo AJAX (Select2) o imporre un campo "ID" con guida nell'interfaccia utente (o una ricerca REST).

Variante 2: visualizza un tipo di post personalizzato (ad esempio, "portfolio") invece degli articoli.

Basta cambiare 'post_type' => 'post' en 'post_type' => 'portfolio' (o una tabella). Considera di rendere configurabile il tipo di post tramite un SELECT se ne hai diversi.

Variante 3 — Memorizzazione nella cache leggera (transitoria) da parte dell'autore

Se il tuo widget viene utilizzato 10 volte in una pagina (questo accade nelle landing page del "team"), puoi nascondere l'elenco dei post. Esempio semplificato:

<?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);
}

Nota: se pubblichi frequentemente, la cache potrebbe ritardare la visualizzazione. Su un sito con traffico elevato, è preferibile utilizzare una cache di oggetti persistente (Redis) e l'invalidazione (hook). save_post) piuttosto che un TTL fisso.

Compatibilità con Divi 5, Elementor e Avada

Elementor (editor e interfaccia utente)

  • Il widget appare nella categoria "Generale".
  • Gli stili sono definiti tramite {{WRAPPER}}che limita le collisioni CSS.
  • Se utilizzi la cache/generazione CSS di Elementor, svuotala dopo aver apportato le modifiche.

Divi 5

Divi non utilizza l'API dei widget di Elementor. Il tuo widget non sarà disponibile in Divi Builder.

Un approccio realistico se devi anche supportare Divi 5:

  • Esporre uno shortcode "compatibile" che riutilizzi la stessa logica PHP (funzione condivisa), quindi inserirlo tramite un modulo Divi Code/Shortcode.
  • Oppure crea un modulo Divi 5 dedicato (più pulito, più lungo).

Avada (Fusion Builder)

Stessa logica: Avada non utilizza i widget di Elementor. Per Avada:

  • Compatibile con gli shortcode (Fusion Builder sa come integrarli).
  • Oppure un elemento Fusion personalizzato se hai bisogno di un'interfaccia utente nativa di Avada.

Un consiglio “multi-costruttore” che applico spesso

Mantieni la logica aziendale in una classe PHP separata (ad esempio, BPCAB_Author_Box_Renderer) e realizzare adattatori:

  • Widget Elementor → richiama il renderer.
  • Shortcode → richiama il renderer.
  • Blocco Gutenberg dinamico → richiama il renderer.

Si evita di duplicare la logica delle query, i fallback e le sequenze di escape.

Controlli post-installazione

  1. Attiva il plugin in Estensioni.
  2. Apri una pagina con Elementor.
  3. Cerca “Inserisci autore (BPCAB)”.
  4. Trascina il widget e inserisci un ID utente valido.
  5. Controlla il lato anteriore:
    • avatar visualizzato
    • nome visualizzato (cliccabile se viene fornito l'URL),
    • Biografia visualizzata se attivata
    • Elenco degli articoli visualizzati e in quantità limitata.
  6. Verifica i controlli di stile: colore di sfondo, tipografia del titolo, spaziatura interna.

Se utilizzi un sistema di caching (plugin di cache, Cloudflare, cache del server), svuotalo. Ho spesso visto sviluppatori credere che "il widget non si carica", quando in realtà il front-end stava servendo una pagina HTML nascosta prima dell'attivazione.

Se non funziona

Lista di controllo rapida (in ordine)

  1. Errore 500 dopo l'attivazione Aspetto wp-content/debug.log (o log del server).
  2. Widget invisibile in Elementor Elementor è attivo? Il hook elementor/widgets/register Ne è affetto?
  3. Classe non trovata Probabilmente hai caricato la classe troppo presto o il percorso require_once è falso.
  4. CSS non applicato Il file si trova nel percorso corretto? L'handle bpcab-author-box È stato salvato correttamente? Cancella la cache di Elementor.
  5. Non viene visualizzato nulla : prova con un ID utente esistente, quindi disattiva "Mostra gli articoli più recenti" per isolare la query.

Tabella diagnostica

sintomo Probabile causa Verifica Soluzione
Widget mancante dall'elenco di Elementor Hook non eseguito / Elementor non caricato Vedi se did_action('elementor/loaded') è vero (log), controlla le estensioni Attiva Elementor, evita init, uso elementor/widgets/register
Errore "Classe 'ElementorWidget_Base' non trovata" Classe piena troppo presto Stack trace in debug.log Spostare la registrazione su elementor/widgets/register e mantenere il require_once in
CSS non caricato Handle non registrato o percorso errato scheda Rete, cerca author-box.css Corretto plugins_url(), dai un'occhiata get_style_depends()svuota la cache
Il nome dell'autore viene visualizzato, ma non l'avatar. Gravatar bloccato / configurazione avatar Impostazioni > Chat > ​​Avatar o restrizioni di rete Consenti Gravatar o utilizza un avatar locale (plugin) / fallback
Elenco vuoto di elementi Autore senza post pubblicati / tipo di post non valido Verifica l'autore dei post e lo stato "Pubblicato". Cambia ID, pubblica un post, regola post_type
cambiamenti non visibili Cache del browser / cache della pagina / cache CSS di Elementor Test in modalità di navigazione privata + svuotamento della cache dei plugin + rigenerazione CSS Svuota la cache, rigenera i file CSS di Elementor se abilitato

Errori e trappole comuni

errore Causare Soluzione
Copia il codice in functions.php tema Posto sbagliato: il tema potrebbe cambiare e l'ordine di caricamento potrebbe non funzionare Elementor Utilizza un plugin dedicato (come questo) o un plugin MU.
Dimenticare un punto e virgola nella classe del widget Errore PHP irreversibile permettere WP_DEBUG_LOGRileggi la riga indicata, usa un IDE
utiliser add_action('init', ...) per salvare il widget Elementor non ancora caricato uso elementor/widgets/register e testare did_action('elementor/loaded')
CSS/JS “non trovato” Percorso sbagliato in plugins_url() oppure il file non è stato creato Verifica la struttura delle directory e i nomi esatti dei file.
Contrasto di stile con il tema (Avada/Divi) CSS troppo generico (ad esempio: .title) Aggiungi un prefisso alle tue classi (bpcab-) e utilizzare {{WRAPPER}} nei selettori di Elementor
Errore dopo l'aggiornamento di PHP Tipizzazione rigorosa + codice vecchio incompatibile Mantieni PHP 8.1+ e correggi gli avvisi/TypeError (nei log)
Test direttamente in produzione Rischio di schermata bianca Staging + distribuzione versionata + rollback
Confusione tra sanificazione e fuga Dati presumibilmente “puliti”. Sanificare gli ingressi, evacuare le uscite, sistematicamente

Consigli su sicurezza, prestazioni e manutenzione

  • Sicurezza XSS : uscire da ogni uscita in base al contesto (esc_html testo, esc_url Url, esc_attr attributo). Riferimento: developer.wordpress.org/apis/security/escaping
  • Permessi Qui non c'è un modulo front-end, quindi nessun nonce. Se aggiungi azioni (ad esempio, un pulsante "segui l'autore"), usa wp_nonce_field e controlla current_user_can.
  • Cookie di prestazione Limita il numero di post e abilita la cache se il widget viene ripetuto. Sui siti web di grandi dimensioni, evita get_users() senza limiti.
  • compatibilità Mantieni le classi e gli handle con un prefisso. Questa è la migliore difesa contro i conflitti di nomi con altri componenti aggiuntivi di Elementor.
  • Manutenzione Controlla la versione del plugin (Git) e includi un registro delle modifiche. Evita di incollare frammenti di codice in un plugin per frammenti: ho visto aggiornamenti compromettere le classi caricate automaticamente perché il plugin per frammenti ha modificato l'ordine di esecuzione.
  • Gestione SEO Questo widget non aggiunge contenuti nascosti, quindi non presenta trappole SEO. Fai attenzione se aggiungi contenuti condizionali utilizzando solo JavaScript.

Risorse

FAQ

Perché creare un plugin invece di inserire il codice nel tema figlio?

Perché un widget di Elementor è una funzionalità. Se cambi tema (o se sostituisci Avada/Divi), vorrai mantenere il widget. Inoltre, eviterai sorprese riguardo all'ordine di caricamento.

Questo widget funziona anche se Elementor Pro non è installato?

Sì. Il codice utilizza l'API dei widget di Elementor (versione gratuita). Elementor Pro offre funzionalità aggiuntive (Theme Builder, ecc.), ma questo widget è indipendente da esso.

Perché non utilizzare uno shortcode in un widget "Shortcode" di Elementor?

Si perdono la maggior parte dei controlli di stile nativi e si finisce per iniettare CSS globale. Per un componente ricorrente, un widget è una soluzione più pulita.

Come faccio ad aggiungere una categoria "BPCAB" in Elementor al posto di "Generale"?

È possibile salvare una categoria Elementor dedicata tramite gli hook di categoria (a seconda della versione di Elementor in uso). Io lo faccio quando ho più di 5 widget; altrimenti, preferisco la categoria "Generale" per evitare di frammentare l'interfaccia utente.

Perché get_settings_for_display() e non get_settings() ?

get_settings_for_display() restituisce valori pronti per la visualizzazione (con alcune trasformazioni interne di Elementor). Questa è generalmente la scelta giusta in render().

Come evitare di inserire un ID utente (non intuitivo)?

utilizzare un SELECT Limita l'accesso (massimo 200 utenti) o implementa un controllo AJAX. Su siti con un elevato volume di traffico, un campo ID e la documentazione interna rappresentano talvolta la soluzione più stabile.

Perché il mio CSS non si aggiorna dopo averlo modificato?

Cache. Cancella:

  • cache del browser
  • la cache del tuo plugin di caching,
  • e se stai utilizzando la generazione CSS di Elementor, forza la rigenerazione (in base alla tua configurazione).

È possibile utilizzare questo widget in un template di Elementor Theme Builder (singolo articolo)?

Sì. In effetti è un buon caso d'uso: un riquadro con il nome dell'autore in fondo a un articolo. In tal caso, migliora la logica per utilizzare l'autore del post corrente se... user_id è vuoto (variante semplice da aggiungere).

Come posso adattare il widget per visualizzare automaticamente l'autore del post corrente?

In render()e $user_id vale 0, recupera get_post_field('post_author', get_the_ID()) Quando ti trovi all'interno di un ciclo/modello, fai attenzione alle pagine statiche che non contengono un contesto di post.

Questo codice è compatibile con PHP 8.2/8.3/8.4?

Sì, in linea di principio (sintassi moderna, tipizzazione rigorosa). Il punto principale a cui prestare attenzione è la compatibilità con Elementor e gli avvisi di deprecazione provenienti da altri plugin.

Come posso testare correttamente il sito senza comprometterne il funzionamento?

Ambiente di staging, log abilitati e un metodo semplice: attiva il plugin, trascina il widget su una pagina di test, controlla il front-end, quindi testa un caso di "autore non trovato" (ID inesistente) per convalidare i fallback.