Si vous avez déjà vu une page blanche après avoir collé “un petit snippet trouvé sur Internet”, vous connaissez le vrai sujet : les bonnes pratiques WordPress ne sont pas une théorie, c’est un filet de sécurité.

Le problème / Le besoin

Sur WordPress 6.9.4 (avril 2026), la majorité des “problèmes WordPress” que je dépanne ne viennent pas du cœur, mais de code ajouté trop vite : un echo dans le mauvais hook, un enqueue mal conditionné, une requête SQL inutile, un formulaire sans nonce, ou un plugin de snippets qui charge avant que WordPress ne soit prêt.

Le besoin est simple : disposer d’un socle de code fiable et réutilisable pour ajouter des fonctionnalités “de blogueur” (CTA, encarts, formulaire, assets) sans casser le site, sans ralentir, et sans ouvrir une faille.

À la fin, vous saurez mettre en place un mini-plugin “mu-plugin” (ou plugin classique) qui illustre des pratiques solides : chargement conditionnel des scripts, shortcode sécurisé, endpoint REST protégé, options stockées proprement, logs propres, et garde-fous (capabilities, nonce, sanitization/escaping).

Résumé rapide

  • On code un mini-plugin (compatible WordPress 6.9.4+ / PHP 8.1+) qui ajoute un encart CTA via shortcode et bloc de compatibilité builders.
  • On charge CSS/JS proprement avec wp_enqueue_scripts, versioning et conditions (éviter de charger partout).
  • On ajoute un endpoint REST pour enregistrer des clics de CTA, avec permissions et validation.
  • On stocke des réglages via l’API Settings (option) et on échappe/sanitize correctement.
  • On vous donne une checklist de tests + un tableau de diagnostic + les pièges que je vois le plus souvent.

Quand utiliser cette solution

  • Vous voulez ajouter une fonctionnalité “métier” simple (CTA, bannière, formulaire léger, tracking minimal) sans installer un plugin lourd.
  • Vous maintenez plusieurs sites et vous voulez un code portable, versionnable (Git), et facile à activer/désactiver.
  • Vous utilisez Divi 5, Elementor ou Avada : vous avez besoin d’un rendu stable via shortcode (universel) et d’un chargement d’assets qui ne pollue pas tout le front.
  • Vous voulez éviter les snippets dispersés dans functions.php (j’ai souvent vu des sites devenir ingérables à cause de ça).

Quand ne PAS utiliser cette solution

  • Vous avez besoin d’un vrai système analytics complet : utilisez un outil dédié, ou une solution conforme RGPD avec consentement. Notre endpoint REST est volontairement minimal.
  • Vous créez un produit distribué publiquement : préférez une architecture complète (classes, autoload, tests, CI, i18n). Ici on vise un “socle pro” mais compact.
  • Vous cherchez une mise en page complexe : faites la mise en forme dans le builder, et gardez le plugin pour la logique (données, sécurité, endpoints, options).
  • Vous n’avez pas la main sur le code (site client verrouillé) : passez par un plugin existant bien maintenu plutôt que d’injecter du code via un plugin de snippets aléatoire.

Prérequis / avant de commencer

  • WordPress : 6.9.4 ou plus récent.
  • PHP : 8.1 minimum (8.2/8.3 recommandé si votre hébergeur suit).
  • Sauvegarde : fichiers + base de données avant toute modification (et pas “plus tard”).
  • Environnement de test : idéalement un staging. Évitez de tester sur production, surtout quand vous touchez aux hooks d’init.
  • Outils utiles :
    • WP-CLI pour vérifier rapidement l’état (wp core version, wp plugin list).
    • Un plugin de logs (ou WP_DEBUG_LOG) pour capturer les erreurs PHP sans casser l’affichage.
  • Sécurité : tout ce qui accepte une entrée (REST, formulaire, query string) doit être validé, sanitize, et protégé par permissions.

Sources officielles à garder sous la main :

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

Le pattern que je vois le plus : coller du code dans functions.php, faire un shortcode qui sort du HTML non échappé, et charger un JS sur toutes les pages “parce que ça marche”.

Exemple naïf (à éviter)

<?php
// ❌ À NE PAS FAIRE : code non conditionné, sorties non échappées, pas de versioning, pas de sécurité.

add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('cta', get_stylesheet_directory_uri() . '/cta.js', array('jquery'));
});

add_shortcode('cta', function ($atts) {
    $url  = $atts['url'];   // ❌ non validé
    $text = $atts['text'];  // ❌ non échappé
    return '<a class="cta" href="' . $url . '">' . $text . '</a>';
});

Pourquoi ça pose problème

  • Sécurité : le shortcode accepte des attributs non validés → XSS possible si un rôle peut publier du contenu (ou si un contenu est importé).
  • Performance : le JS est chargé partout, même là où le CTA n’existe pas.
  • Maintenance : functions.php dépend du thème. Vous changez de thème (ou vous mettez à jour sans thème enfant) et vous perdez tout.
  • Débogage : une simple parenthèse oubliée → écran blanc. Et en prod, sans logs, vous cherchez à l’aveugle.

La bonne approche — tutoriel pas à pas

Étape 1 — Choisir où mettre le code (plugin ou mu-plugin)

Pour un site de blog, je recommande souvent un mu-plugin si la fonctionnalité doit rester active quel que soit le thème. Sinon, un plugin classique fait très bien le travail.

  • Option A (recommandée) : mu-plugin : wp-content/mu-plugins/bpcab-cta/bpcab-cta.php
  • Option B : plugin classique : wp-content/plugins/bpcab-cta/bpcab-cta.php

Si vous choisissez mu-plugin, créez le dossier mu-plugins s’il n’existe pas.

Étape 2 — Déclarer le plugin, bloquer les accès directs, fixer une version

Le but : éviter l’exécution hors WordPress, et avoir une constante de version pour le cache des assets.

Étape 3 — Ajouter un shortcode sûr (sanitize + escaping)

On va créer [bpcab_cta]. Il générera un lien CTA avec un identifiant. Cet identifiant servira au tracking côté REST.

Étape 4 — Charger CSS/JS uniquement si nécessaire

Deux stratégies fiables :

  • Simple : charger sur toutes les pages (OK si très léger, mais rarement optimal).
  • Propre : charger seulement si le shortcode est présent dans le contenu, ou si un builder a rendu le shortcode.

Dans mon expérience, la détection “shortcode dans le contenu” couvre 80% des cas. Pour Divi/Elementor/Avada, on ajoute un filet de sécurité : si le script est demandé explicitement via un hook, on le charge.

Étape 5 — Créer un endpoint REST protégé

On expose /wp-json/bpcab/v1/cta-click qui accepte un cta_id. On valide le format, on limite les abus (throttle léger), et on stocke un compteur en option (simple) ou en meta (variante).

Étape 6 — Ajouter une page Réglages minimaliste (option)

On ajoute un réglage : activer/désactiver le tracking REST. Ça évite de “commenter du code” en prod.

Code complet

Copiez-collez ce fichier tel quel. Il est volontairement monolithique pour rester lisible. Sur un projet plus gros, je factoriserais en classes.

<?php
/**
 * Plugin Name: BPCAB CTA (bonnes pratiques)
 * Description: Ajoute un shortcode CTA, charge les assets proprement, et propose un endpoint REST sécurisé pour compter les clics.
 * Version: 1.0.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 * Author: Votre équipe
 *
 * Ce plugin est conçu pour WordPress 6.9.4+ (avril 2026).
 */

declare(strict_types=1);

if (!defined('ABSPATH')) {
	exit; // Empêche l'accès direct au fichier.
}

define('BPCAB_CTA_VERSION', '1.0.0');
define('BPCAB_CTA_SLUG', 'bpcab-cta');
define('BPCAB_CTA_OPT', 'bpcab_cta_options');

/**
 * Retourne les options du plugin avec des valeurs par défaut.
 */
function bpcab_cta_get_options(): array {
	$defaults = array(
		'enable_tracking' => true,
	);

	$opts = get_option(BPCAB_CTA_OPT, array());
	if (!is_array($opts)) {
		$opts = array();
	}

	// Fusion simple, sans surprises.
	return array_merge($defaults, $opts);
}

/**
 * Enregistre le shortcode [bpcab_cta].
 */
function bpcab_cta_register_shortcode(): void {
	add_shortcode('bpcab_cta', 'bpcab_cta_shortcode_render');
}
add_action('init', 'bpcab_cta_register_shortcode');

/**
 * Rend le shortcode.
 *
 * Exemples :
 * [bpcab_cta text="Télécharger le guide" url="https://exemple.com/guide" id="guide-2026" rel="nofollow"]
 *
 * Sécurité :
 * - Validation URL
 * - Escaping HTML
 */
function bpcab_cta_shortcode_render($atts = array(), $content = null, $tag = ''): string {
	$atts = shortcode_atts(
		array(
			'text' => 'En savoir plus',
			'url'  => '',
			'id'   => '',
			'rel'  => 'noopener',
			'target' => '_blank',
			'class' => '',
		),
		(array) $atts,
		'bpcab_cta'
	);

	$text = sanitize_text_field((string) $atts['text']);
	$url  = esc_url_raw((string) $atts['url']);

	// Un identifiant simple (pour tracking). Si absent, on en génère un stable basé sur l'URL.
	$id = sanitize_key((string) $atts['id']);
	if ($id === '' && $url !== '') {
		$id = 'cta-' . substr(md5($url), 0, 10);
	}

	$rel = sanitize_text_field((string) $atts['rel']);
	$target = sanitize_text_field((string) $atts['target']);

	$extra_class = sanitize_html_class((string) $atts['class']);
	$classes = trim('bpcab-cta ' . $extra_class);

	// Si pas d'URL valide, on ne rend rien (évite un lien vide).
	if ($url === '') {
		return '';
	}

	// Marqueur pour permettre le chargement conditionnel des assets.
	$GLOBALS['bpcab_cta_has_cta'] = true;

	// Data attribute pour le JS.
	$data_id_attr = $id !== '' ? ' data-cta-id="' . esc_attr($id) . '"' : '';

	return '<a class="' . esc_attr($classes) . '" href="' . esc_url($url) . '" rel="' . esc_attr($rel) . '" target="' . esc_attr($target) . '"' . $data_id_attr . '>'
		. esc_html($text)
		. '</a>';
}

/**
 * Enqueue des assets, si nécessaire.
 *
 * Notes :
 * - On évite de charger partout.
 * - On ajoute une variable JS localisée avec le nonce REST.
 */
function bpcab_cta_enqueue_assets(): void {
	$should_enqueue = false;

	// 1) Si un shortcode a été rendu (plus fiable que "has_shortcode" quand le contenu est filtré par un builder).
	if (!empty($GLOBALS['bpcab_cta_has_cta'])) {
		$should_enqueue = true;
	}

	// 2) Fallback : si le post courant contient le shortcode (utile si le rendu n'a pas encore eu lieu).
	if (!$should_enqueue && is_singular()) {
		$post = get_post();
		if ($post instanceof WP_Post) {
			// has_shortcode attend du contenu brut, ça marche dans la plupart des cas.
			if (has_shortcode((string) $post->post_content, 'bpcab_cta')) {
				$should_enqueue = true;
			}
		}
	}

	/**
	 * 3) Filtre pour forcer l'enqueue depuis un thème/builder.
	 * Exemple : add_filter('bpcab_cta_force_enqueue', '__return_true');
	 */
	$should_enqueue = (bool) apply_filters('bpcab_cta_force_enqueue', $should_enqueue);

	if (!$should_enqueue) {
		return;
	}

	$base_url = plugin_dir_url(__FILE__);

	wp_enqueue_style(
		'bpcab-cta',
		$base_url . 'assets/cta.css',
		array(),
		BPCAB_CTA_VERSION
	);

	wp_enqueue_script(
		'bpcab-cta',
		$base_url . 'assets/cta.js',
		array(),
		BPCAB_CTA_VERSION,
		true
	);

	$options = bpcab_cta_get_options();

	wp_localize_script(
		'bpcab-cta',
		'BPCAB_CTA',
		array(
			'restUrl' => esc_url_raw(rest_url('bpcab/v1/cta-click')),
			// Nonce REST : côté serveur, on vérifie via check_ajax_referer-like ? Non, pour REST on lit l'en-tête X-WP-Nonce.
			'nonce'   => wp_create_nonce('wp_rest'),
			'enableTracking' => (bool) $options['enable_tracking'],
		)
	);
}
add_action('wp_enqueue_scripts', 'bpcab_cta_enqueue_assets');

/**
 * Déclare la route REST.
 */
function bpcab_cta_register_rest_routes(): void {
	register_rest_route(
		'bpcab/v1',
		'/cta-click',
		array(
			'methods'  => WP_REST_Server::CREATABLE, // POST
			'callback' => 'bpcab_cta_rest_click',
			'permission_callback' => 'bpcab_cta_rest_permissions',
			'args' => array(
				'cta_id' => array(
					'type' => 'string',
					'required' => true,
					'sanitize_callback' => 'sanitize_key',
					'validate_callback' => function ($param) {
						$param = (string) $param;
						// Limite simple : évite les IDs vides et trop longs.
						return $param !== '' && strlen($param) <= 64;
					},
				),
			),
		)
	);
}
add_action('rest_api_init', 'bpcab_cta_register_rest_routes');

/**
 * Permissions REST :
 * - Endpoint public, mais protégé par nonce REST + option d'activation.
 * - On limite aussi le spam via un throttle basique par IP.
 *
 * Attention : si vous voulez du tracking sans nonce (visiteurs non connectés),
 * vous devez gérer le consentement et la protection anti-abus autrement.
 */
function bpcab_cta_rest_permissions(WP_REST_Request $request): bool {
	$options = bpcab_cta_get_options();
	if (empty($options['enable_tracking'])) {
		return false;
	}

	// Vérifie le nonce REST (header X-WP-Nonce).
	$nonce = $request->get_header('x_wp_nonce');
	if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
		return false;
	}

	// Throttle léger : 1 hit / 2s / IP (approximatif).
	$ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '';
	$key = 'bpcab_cta_throttle_' . md5($ip);
	$last = (int) get_transient($key);
	$now = time();
	if ($last > 0 && ($now - $last) < 2) {
		return false;
	}
	set_transient($key, $now, 10);

	return true;
}

/**
 * Callback REST : incrémente un compteur par cta_id.
 * Stockage simple en option (table wp_options).
 */
function bpcab_cta_rest_click(WP_REST_Request $request): WP_REST_Response {
	$cta_id = (string) $request->get_param('cta_id');
	$cta_id = sanitize_key($cta_id);

	$counts = get_option('bpcab_cta_counts', array());
	if (!is_array($counts)) {
		$counts = array();
	}

	$current = isset($counts[$cta_id]) ? (int) $counts[$cta_id] : 0;
	$counts[$cta_id] = $current + 1;

	// Autoload = no pour éviter de gonfler les options chargées sur chaque page.
	update_option('bpcab_cta_counts', $counts, false);

	return new WP_REST_Response(
		array(
			'ok' => true,
			'cta_id' => $cta_id,
			'count' => (int) $counts[$cta_id],
		),
		200
	);
}

/**
 * Ajoute une page de réglages minimaliste (Réglages > BPCAB CTA).
 */
function bpcab_cta_register_settings_page(): void {
	add_options_page(
		'BPCAB CTA',
		'BPCAB CTA',
		'manage_options',
		'bpcab-cta',
		'bpcab_cta_render_settings_page'
	);
}
add_action('admin_menu', 'bpcab_cta_register_settings_page');

/**
 * Enregistre le setting.
 */
function bpcab_cta_register_settings(): void {
	register_setting(
		'bpcab_cta_settings',
		BPCAB_CTA_OPT,
		array(
			'type' => 'array',
			'sanitize_callback' => 'bpcab_cta_sanitize_options',
			'default' => array(
				'enable_tracking' => true,
			),
		)
	);

	add_settings_section(
		'bpcab_cta_main',
		'Réglages',
		function () {
			echo '<p>Réglages simples. Le tracking REST nécessite un nonce (visiteurs connectés).</p>';
		},
		'bpcab-cta'
	);

	add_settings_field(
		'enable_tracking',
		'Activer le tracking des clics (REST)',
		'bpcab_cta_field_enable_tracking',
		'bpcab-cta',
		'bpcab_cta_main'
	);
}
add_action('admin_init', 'bpcab_cta_register_settings');

/**
 * Sanitize des options.
 */
function bpcab_cta_sanitize_options($input): array {
	$input = is_array($input) ? $input : array();

	return array(
		'enable_tracking' => !empty($input['enable_tracking']),
	);
}

/**
 * Champ checkbox.
 */
function bpcab_cta_field_enable_tracking(): void {
	$options = bpcab_cta_get_options();
	$checked = !empty($options['enable_tracking']) ? 'checked' : '';

	echo '<label>';
	echo '<input type="checkbox" name="' . esc_attr(BPCAB_CTA_OPT) . '[enable_tracking]" value="1" ' . $checked . ' /> ';
	echo 'Compter les clics via l’endpoint REST (nonce requis).';
	echo '</label>';
}

/**
 * Render page settings.
 */
function bpcab_cta_render_settings_page(): void {
	if (!current_user_can('manage_options')) {
		return;
	}

	echo '<div class="wrap">';
	echo '<h1>BPCAB CTA</h1>';
	echo '<form method="post" action="options.php">';

	settings_fields('bpcab_cta_settings');
	do_settings_sections('bpcab-cta');
	submit_button();

	echo '</form>';
	echo '</div>';
}

Fichier CSS

Créez assets/cta.css dans le même dossier que le plugin.

.bpcab-cta{
  display:inline-block;
  padding:0.7rem 1rem;
  border-radius:10px;
  background:#111827;
  color:#fff;
  text-decoration:none;
  font-weight:600;
  line-height:1.2;
}
.bpcab-cta:hover,
.bpcab-cta:focus{
  background:#0b1220;
  color:#fff;
}

Fichier JS

Créez assets/cta.js. Ce JS envoie un POST REST au clic, si activé.

(function () {
  "use strict";

  function postJson(url, data, nonce) {
    return fetch(url, {
      method: "POST",
      credentials: "same-origin",
      headers: {
        "Content-Type": "application/json",
        "X-WP-Nonce": nonce
      },
      body: JSON.stringify(data)
    });
  }

  document.addEventListener("click", function (e) {
    var a = e.target.closest && e.target.closest("a.bpcab-cta");
    if (!a) return;

    if (typeof window.BPCAB_CTA === "undefined") return;
    if (!window.BPCAB_CTA.enableTracking) return;

    var ctaId = a.getAttribute("data-cta-id");
    if (!ctaId) return;

    // Fire-and-forget : on ne bloque pas la navigation.
    try {
      postJson(window.BPCAB_CTA.restUrl, { cta_id: ctaId }, window.BPCAB_CTA.nonce);
    } catch (err) {
      // Pas de console.error agressif en prod.
    }
  });
})();

Explication du code

Logique globale (simple)

  • Le shortcode rend un lien CTA propre et marque un drapeau global ($GLOBALS['bpcab_cta_has_cta']).
  • Au moment d’enqueue, on charge CSS/JS seulement si on a détecté le CTA (drapeau, ou has_shortcode, ou filtre).
  • Le JS envoie un POST REST au clic, avec un nonce REST.
  • Le serveur vérifie l’option, le nonce, et applique un throttle minimal, puis incrémente un compteur.

Détails techniques qui font la différence

  • Escaping vs sanitize :
    • sanitize nettoie l’entrée (ex: sanitize_text_field, sanitize_key).
    • escape protège la sortie (ex: esc_html, esc_attr, esc_url).
  • Nonce REST :
    • On génère wp_create_nonce('wp_rest') côté PHP.
    • On l’envoie via X-WP-Nonce côté JS.
    • On vérifie avec wp_verify_nonce dans permission_callback.
  • Autoload des options :
    • Le compteur est stocké dans une option non autoloadée (update_option(..., false)) pour éviter de gonfler wp_options chargée à chaque requête.
  • Hooking :
    • init pour le shortcode (WordPress est prêt à enregistrer les shortcodes).
    • wp_enqueue_scripts pour les assets front.
    • rest_api_init pour enregistrer la route REST.
    • admin_init + admin_menu pour Settings.

Docs utiles :

Variantes et cas d'usage

Variante 1 — Désactiver le tracking pour les visiteurs non connectés (comportement actuel)

Le code tel quel fonctionne surtout pour les utilisateurs connectés (nonce REST). Pour un blog public, c’est souvent ce que vous voulez si vous n’avez pas de gestion de consentement et d’anti-abus.

Variante 2 — Stocker les clics par post (post meta) plutôt qu’en option

Si votre CTA est lié à un article, stockez dans le post meta. Avantage : requêtes ciblées, export plus simple par post. Inconvénient : vous devez envoyer le post_id et vérifier qu’il existe.

<?php
// Exemple partiel : logique de stockage en post meta (à intégrer dans le callback REST).
$post_id = (int) $request->get_param('post_id');
if ($post_id > 0 && get_post_status($post_id)) {
	$key = '_bpcab_cta_clicks_' . $cta_id;
	$current = (int) get_post_meta($post_id, $key, true);
	update_post_meta($post_id, $key, $current + 1);
}

Variante 3 — Forcer le chargement des assets sur une page builder spécifique

Quand un builder “recompose” le contenu, has_shortcode peut rater le coche. Le filtre bpcab_cta_force_enqueue est là pour ça.

<?php
// À placer dans un plugin site-specific ou thème enfant.
add_filter('bpcab_cta_force_enqueue', function ($should) {
	// Exemple : forcer sur une page ID 123.
	if (is_page(123)) {
		return true;
	}
	return $should;
});

Compatibilité Divi 5 / Elementor / Avada

Divi 5

Divi rend très bien les shortcodes dans un module “Code” ou “Texte”. Le point d’attention : Divi peut mettre en cache le rendu. Si vous modifiez le CSS/JS et que vous ne voyez rien, videz :

  • Divi > Options du thème > Builder > cache (selon configuration)
  • Votre plugin de cache
  • Le cache navigateur

Si l’enqueue conditionnel ne se déclenche pas (rare, mais je l’ai vu sur des layouts très dynamiques), utilisez le filtre bpcab_cta_force_enqueue sur la page concernée.

Elementor

Elementor a un widget “Shortcode”. Le shortcode est généralement présent dans post_content, donc la détection fonctionne. Si vous utilisez des templates et des loops, vous pouvez avoir des cas où le CTA est rendu via un template sans que le contenu “principal” contienne le shortcode. Là encore : filtre de forçage.

Avada (Fusion Builder)

Avada supporte les shortcodes dans ses éléments texte/code. J’ai souvent vu un problème de CSS non appliqué parce qu’Avada combine/minifie les assets. Testez d’abord sans optimisation Avada, puis réactivez.

Recommandation pratique (builders)

Pour les builders, je garde le rendu dans le builder (mise en page), et je confie au plugin :

  • la génération du CTA (shortcode),
  • le tracking,
  • le chargement propre des assets.

Vérifications après mise en place

  • Vérifiez que le shortcode rend bien un lien : ajoutez [bpcab_cta text="Tester" url="https://example.com" id="test"] dans un article.
  • Ouvrez la page et vérifiez que assets/cta.css et assets/cta.js sont chargés (onglet Réseau).
  • Si vous êtes connecté, cliquez le CTA et vérifiez la requête POST vers /wp-json/bpcab/v1/cta-click (statut 200).
  • Dans l’admin, allez dans Réglages > BPCAB CTA, désactivez le tracking, puis retestez : la requête devrait échouer (403 ou 401 selon contexte).

Tableau de diagnostic (symptôme → solution)

Symptôme Cause probable Vérification Solution
Le shortcode affiche du texte brut Shortcode non enregistré (mauvais hook / plugin non chargé) Vérifier que le plugin est actif (ou mu-plugin bien placé) Mettre le fichier au bon endroit, vérifier add_action('init', ...)
Le CTA s’affiche mais sans style CSS non chargé (enqueue non déclenché / cache) Onglet Réseau : cta.css absent Forcer l’enqueue via filtre, vider les caches (plugin + builder + navigateur)
Erreur REST 403 Nonce REST absent/invalide Requête contient-elle X-WP-Nonce ? Vérifier wp_localize_script, être connecté, éviter un cache qui sert une page sans nonce
Compteur ne monte jamais Tracking désactivé Réglages > BPCAB CTA Réactiver “Activer le tracking…”
Page blanche après ajout du plugin Erreur PHP (point-virgule, parenthèse, PHP < 8.1) Consulter wp-content/debug.log ou logs serveur Corriger l’erreur, vérifier la version PHP, déployer via staging

Si ça ne marche pas

1) Vérifiez l’emplacement du code

  • Mu-plugin : wp-content/mu-plugins/bpcab-cta/bpcab-cta.php (le fichier doit être directement lisible par WordPress).
  • Plugin : wp-content/plugins/bpcab-cta/bpcab-cta.php + activation dans l’admin.

Erreur fréquente : créer mu-plugins/bpcab-cta.php mais mettre les assets dans un autre dossier. Ici, on suppose assets dans le même dossier que le fichier PHP.

2) Activez les logs proprement

Sur un staging, activez :

<?php
// wp-config.php (staging uniquement)
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

Puis relisez wp-content/debug.log.

3) Vérifiez le hook d’enqueue

J’ai souvent croisé un snippet collé sur init pour enqueuer des scripts. Ça “marche parfois” puis casse avec un cache ou un builder. Ici, on reste sur wp_enqueue_scripts.

4) Videz les caches (dans le bon ordre)

  1. Cache plugin (page cache / object cache).
  2. Cache du builder (Divi/Avada).
  3. Cache CDN si présent.
  4. Cache navigateur.

5) Testez l’endpoint REST isolément

Si vous êtes connecté, testez avec curl (en récupérant un nonce via la page, ou via un petit script). Exemple (pseudo) :

curl -X POST "https://votre-site.tld/wp-json/bpcab/v1/cta-click" 
  -H "Content-Type: application/json" 
  -H "X-WP-Nonce: VOTRE_NONCE" 
  --data '{"cta_id":"test"}'

Pièges et erreurs courantes

Erreur Cause Solution
Copier le code dans functions.php du thème parent Mise à jour du thème = perte du code Utiliser un plugin (ou mu-plugin) ou un thème enfant
Oublier un point-virgule → écran blanc Erreur de syntaxe PHP Lire les logs, corriger, tester sur staging
Utiliser le mauvais hook pour enqueuer init au lieu de wp_enqueue_scripts Enqueue uniquement dans wp_enqueue_scripts / admin_enqueue_scripts
Conflit de cache : nonce REST invalide Une page cache sert un nonce expiré ou d’un autre utilisateur Exclure les pages pour utilisateurs connectés du cache, ou désactiver tracking public
“REST 403” même connecté Header X-WP-Nonce absent (JS non chargé) Vérifier l’enqueue conditionnel, forcer via filtre
Shortcode fonctionne dans l’éditeur, pas dans le builder Le builder rend via template/loop hors post_content Forcer l’enqueue sur la page (filtre) ou charger globalement si très léger
“Call to undefined function …” Code exécuté trop tôt (avant chargement WP) ou fichier inclus hors contexte Vérifier ABSPATH, placer le code dans un plugin, utiliser les hooks
Erreur “Your PHP version is too old” Hébergement en PHP < 8.1 Mettre à jour PHP côté hébergeur (ou adapter le code, mais déconseillé)
CSS/JS ne se charge pas Mauvais chemin plugin_dir_url(__FILE__) si assets mal placés Respecter l’arborescence, vérifier les 404 dans l’onglet Réseau

Conseils sécurité, performance et maintenance

Sécurité

  • Ne rendez jamais du HTML non échappé depuis un shortcode, même si “seuls les admins” l’utilisent. Les contenus sont copiés, importés, et parfois édités par d’autres rôles.
  • REST public = surface d’attaque. Ici, on a volontairement exigé un nonce REST, ce qui limite l’usage aux sessions connectées. Pour du tracking public, il faut une stratégie complète (consentement + anti-abus + stockage).
  • Permissions : pour une route qui modifie des données sensibles, utilisez des capabilities. Ici, on incrémente un compteur, mais on limite quand même.

Performance

  • Chargez les assets seulement quand nécessaire. C’est un gain direct sur Core Web Vitals si votre site a beaucoup de pages.
  • Évitez de gonfler wp_options autoload. Les options autoloadées sont chargées sur (presque) chaque requête. Les compteurs volumineux doivent être autoload=no ou stockés ailleurs.
  • Attention au stockage en option : si vous avez des centaines de CTA, l’option bpcab_cta_counts peut grossir. Dans ce cas, passez à une table dédiée ou au post meta.

Maintenance

  • Versionnez ce plugin (Git). Un snippet non versionné devient vite un “mystère” lors d’une refonte.
  • Documentez les filtres que vous exposez (ici bpcab_cta_force_enqueue). C’est le genre de détail qui vous sauve quand un builder change son rendu.
  • Évitez les tutos anciens : beaucoup de snippets pré-PHP 8.1 utilisent des patterns fragiles (globals non contrôlés, fonctions obsolètes, JS inline). Sur WordPress 6.9.4, restez sur les APIs officielles.

Ressources

FAQ

Pourquoi un plugin plutôt que functions.php ?

Parce que le code n’est pas censé dépendre du thème. Un plugin survit aux changements de thème, et se désactive proprement pour isoler un bug.

Pourquoi le tracking REST exige un nonce ?

Parce que sinon, votre endpoint devient un point d’entrée public facile à spammer. Avec un nonce REST, vous limitez (par défaut) aux utilisateurs connectés. Pour du tracking public, il faut un design différent.

Je veux compter les clics des visiteurs non connectés, comment faire ?

Techniquement : vous pouvez autoriser sans nonce et mettre un anti-abus plus sérieux (rate limiting, signature, stockage côté serveur). Légalement : vous devez gérer le consentement selon votre contexte (RGPD). Je vous déconseille de “juste ouvrir la route”.

Pourquoi utiliser update_option(..., false) ?

Le troisième paramètre force autoload=no. Un compteur peut grossir ; vous ne voulez pas qu’il soit chargé à chaque page.

Le shortcode ne déclenche pas l’enqueue dans mon builder, que faire ?

Utilisez le filtre bpcab_cta_force_enqueue sur la page concernée, ou chargez globalement si votre CSS/JS est minuscule. Sur certains templates de loop, has_shortcode ne voit rien.

Pourquoi un “drapeau global” $GLOBALS['bpcab_cta_has_cta'] ?

Parce que c’est un moyen simple et fiable de savoir qu’un CTA a été rendu, même si le contenu a été transformé par des filtres complexes. C’est un vieux truc, mais efficace quand on l’utilise avec parcimonie.

Est-ce compatible multisite ?

Oui, au niveau basique : options et transients sont par site. Si vous voulez un reporting réseau, il faut stocker au niveau réseau (site option) ou centraliser ailleurs.

Est-ce que ça respecte les standards WordPress ?

On utilise les APIs standards (shortcode, enqueue, REST, Settings). Pour aller plus loin : séparer en classes, ajouter des tests, et documenter plus finement les filtres/actions.

Comment afficher les compteurs dans l’admin ?

Le plus simple : une page d’admin dédiée qui lit get_option('bpcab_cta_counts') et affiche un tableau. Si vous avez beaucoup d’IDs, prévoyez pagination et nettoyage.

Que faire si j’ai une erreur “REST cookie is invalid nonce” ?

Ça arrive souvent avec un cache agressif pour utilisateurs connectés, ou un plugin de sécurité qui modifie les cookies. Désactivez temporairement le cache pour les connectés, puis retestez. Vérifiez aussi que le header X-WP-Nonce est bien envoyé.

Puis-je remplacer wp_localize_script ?

Oui : vous pouvez utiliser wp_add_inline_script pour injecter un objet JSON. wp_localize_script reste très pratique et largement utilisé pour passer des données au JS.