Si vous avez déjà passé 20 minutes à comprendre pourquoi votre add_action() “ne renvoie rien”, vous avez probablement utilisé une action là où un filtre était attendu (ou l’inverse).

WordPress 6.9.4 (avril 2026) utilise toujours le même système de hooks qu’en 2005, mais les erreurs qu’on voit aujourd’hui sont plus subtiles : hooks déclenchés plus tôt/trop tard, mauvais nombre d’arguments, priorités qui se marchent dessus, et snippets collés dans le mauvais fichier.

Le problème / Le besoin

Votre site WordPress est un empilement de comportements : le cœur, le thème, les plugins, parfois un builder (Divi 5, Elementor, Avada). Vous voulez modifier un détail sans forker un plugin, sans toucher aux fichiers du cœur, et sans casser les mises à jour.

Les hooks sont l’outil principal pour ça. Sauf que la confusion add_action vs add_filter est l’une des causes les plus fréquentes de bugs “silencieux” : rien ne s’affiche, aucune erreur, mais votre code ne fait pas ce que vous pensez.

À la fin, vous saurez :

  • choisir rapidement entre action et filtre (et reconnaître les hooks “pièges”),
  • écrire des callbacks corrects (signature, priorité, nombre d’arguments),
  • tester proprement vos hooks sur WordPress 6.9.4 avec PHP 8.1+,
  • déboguer les cas où “ça ne se déclenche pas”.

Résumé rapide

  • Une action déclenche un effet de bord (enqueue, log, email, insertion de HTML, etc.).
  • Un filtre reçoit une valeur, la transforme, et doit la retourner.
  • Techniquement, add_action() et add_filter() appellent le même mécanisme interne : add_filter(). La différence est conceptuelle et se joue dans la valeur de retour.
  • On va coder un mini-plugin “Hooks Lab” : une action qui ajoute un badge dans le footer admin + un filtre qui modifie l’extrait et un filtre qui ajuste les arguments de requête.
  • On ajoute un écran d’options (avec nonce + capabilities) pour activer/désactiver nos hooks sans toucher au code.
  • On termine avec une méthode de test/débogage (priorités, doing_filter(), did_action(), Query Monitor).

Quand utiliser cette solution

  • Vous devez modifier un comportement d’un plugin/thème sans éditer ses fichiers.
  • Vous voulez injecter du HTML à un endroit précis (action), ou modifier un texte/une valeur (filtre).
  • Vous cherchez une approche maintenable pour un site client (mini-plugin plutôt que “functions.php” qui grossit).
  • Vous avez besoin d’un mécanisme activable/désactivable via une option (utile quand plusieurs devs interviennent).
  • Vous intervenez sur des sites builder-heavy (Divi/Elementor/Avada) où la plupart des modifications passent par hooks + shortcodes/widgets.

Quand ne PAS utiliser cette solution

  • Vous voulez “refaire la mise en page” d’une page builder : utilisez plutôt les templates du builder, un thème enfant, ou les templates de blocs. Les hooks sont bons pour des points d’extension, pas pour reconstruire une page entière.
  • Vous cherchez à corriger un plugin cassé : si le plugin n’expose aucun hook, un fork ou une PR est parfois plus sain.
  • Vous voulez modifier la base de données directement à chaque chargement (anti-pattern). Préférez une migration (WP-CLI) ou une action à l’activation.
  • Vous devez manipuler des données sensibles (tokens, PII) via hooks “front” : attention aux fuites via caches/CDN. Dans ce cas, isolez côté serveur et logguez proprement.

Prérequis / avant de commencer

Environnement recommandé :

  • WordPress 6.9.4 (ou supérieur) et PHP 8.1+.
  • Un site de test (staging) ou un environnement local.
  • Un éditeur avec lint PHP (VS Code + PHP Intelephense, PhpStorm, etc.).

Outils utiles :

  • Query Monitor pour voir hooks, requêtes, erreurs PHP.
  • Accès aux logs PHP (ou WP_DEBUG_LOG en staging uniquement).

Précautions :

  • Ne testez pas un snippet de hook directement en production sans sauvegarde (j’ai souvent vu un simple point-virgule manquant provoquer un écran blanc).
  • Si vous utilisez un plugin de snippets, vérifiez qu’il n’exécute pas le code trop tôt/trop tard (certains chargent avant le thème, ce qui change la disponibilité de certains hooks).

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

Voici le genre de code que je vois régulièrement dans un functions.php : une action utilisée comme un filtre, et un filtre qui “echo” au lieu de retourner.

<?php
// ❌ Mauvais : utiliser une action pour modifier une valeur attendue en retour.
add_action('the_content', function ($content) {
    // Le développeur pense modifier le contenu...
    $content .= '<p>Texte ajouté</p>';
    // ...mais il oublie de retourner $content.
});

// ❌ Mauvais : echo dans un filtre.
add_filter('excerpt_length', function ($length) {
    echo 20; // Un filtre doit retourner une valeur, pas l'afficher.
});

Pourquoi c’est un problème :

  • Bug silencieux : WordPress n’affiche pas forcément d’erreur, mais votre modification ne s’applique pas.
  • Effets de bord : un echo dans un filtre peut casser des réponses JSON (REST API), du RSS, ou des requêtes AJAX.
  • Maintenabilité : dans 6 mois, quelqu’un va “corriger” en ajoutant un second hook, et vous aurez deux comportements concurrents.

La bonne approche — tutoriel pas à pas

Étape 1 — Créez un mini-plugin (plutôt que functions.php)

Oui, vous pouvez mettre ça dans un thème enfant. Mais dans mon expérience, les hooks “fonctionnels” (SEO, excerpts, requêtes, admin) vivent mieux dans un plugin : ils survivent au changement de thème, y compris avec Divi/Avada.

Créez le fichier : wp-content/plugins/hooks-lab/hooks-lab.php

Étape 2 — Ajoutez un réglage simple (option) pour activer/désactiver

Objectif : pouvoir couper vos hooks sans supprimer le code (utile en dépannage).

Étape 3 — Exemple d’action (effet de bord)

On ajoute un badge dans le footer de l’admin avec admin_footer_text (filtre) ou admin_notices (action). Ici, je choisis une action (admin_notices) car on veut afficher un bloc HTML à un endroit donné.

Étape 4 — Exemple de filtre (transformer une valeur)

On modifie la longueur de l’extrait et on ajoute un suffixe à la fin. Là, c’est clairement un filtre : on reçoit une valeur et on la retourne.

Étape 5 — Exemple “piège” : hook qui ressemble à une action, mais qui est un filtre

the_content est un filtre (malgré son nom). Beaucoup de devs l’accrochent avec add_action() parce qu’ils veulent “agir quand le contenu s’affiche”. Techniquement, ça marche… jusqu’au jour où votre callback ne retourne plus la valeur et vous cassez l’affichage.

Je recommande de l’accrocher avec add_filter par discipline.

Étape 6 — Protégez l’admin (capabilities + nonce)

On ajoute une page dans Réglages pour activer/désactiver nos hooks. Vous verrez passer :

  • current_user_can() pour les permissions,
  • wp_nonce_field() / check_admin_referer() contre le CSRF,
  • sanitize_text_field() / absint() pour nettoyer les entrées.

Code complet

Copiez-collez ce fichier tel quel dans wp-content/plugins/hooks-lab/hooks-lab.php, puis activez le plugin.

<?php
/**
 * Plugin Name: Hooks Lab (Actions vs Filtres)
 * Description: Démonstrations concrètes de add_action() vs add_filter() + écran de réglages.
 * Version: 1.0.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 * Author: Votre Nom
 */

declare(strict_types=1);

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

final class BPCAB_Hooks_Lab {

	private const OPTION_KEY = 'bpcab_hooks_lab_settings';

	public static function boot(): void {
		// Admin.
		add_action('admin_menu', [__CLASS__, 'register_settings_page']);
		add_action('admin_init', [__CLASS__, 'maybe_save_settings']);

		// Démo action : afficher un avis admin (effet de bord).
		add_action('admin_notices', [__CLASS__, 'maybe_print_admin_notice']);

		// Démo filtres : transformer des valeurs.
		add_filter('excerpt_length', [__CLASS__, 'filter_excerpt_length'], 20, 1);
		add_filter('excerpt_more', [__CLASS__, 'filter_excerpt_more'], 20, 1);

		// Hook "piège" : the_content est un filtre.
		add_filter('the_content', [__CLASS__, 'filter_the_content_append_badge'], 12, 1);

		// Démo filtre orienté requête (front) : modifier la requête principale sur la home.
		add_action('pre_get_posts', [__CLASS__, 'action_pre_get_posts_adjust_home_query']);
	}

	/**
	 * Valeurs par défaut.
	 */
	private static function defaults(): array {
		return [
			'enable_admin_notice'   => 1,
			'enable_excerpt_tweaks' => 1,
			'enable_content_badge'  => 1,
			'enable_home_query'     => 0,
		];
	}

	/**
	 * Récupère les réglages en fusionnant avec les valeurs par défaut.
	 */
	private static function get_settings(): array {
		$raw = get_option(self::OPTION_KEY, []);
		if (!is_array($raw)) {
			$raw = [];
		}
		return array_merge(self::defaults(), $raw);
	}

	public static function register_settings_page(): void {
		add_options_page(
			'Hooks Lab',
			'Hooks Lab',
			'manage_options',
			'bpcab-hooks-lab',
			[__CLASS__, 'render_settings_page']
		);
	}

	public static function render_settings_page(): void {
		if (!current_user_can('manage_options')) {
			wp_die(esc_html__('Vous n’avez pas les permissions nécessaires.', 'default'));
		}

		$settings = self::get_settings();
		?>
		<div class="wrap">
			<h1>Hooks Lab — Actions vs Filtres</h1>

			<p>
				Ces réglages activent/désactivent des exemples de hooks.
				Pratique pour tester des priorités et vérifier vos hypothèses.
			</p>

			<form method="post" action="">
				<?php wp_nonce_field('bpcab_hooks_lab_save', 'bpcab_hooks_lab_nonce'); ?>

				<table class="form-table" role="presentation">
					<tbody>
						<tr>
							<th scope="row">Avis admin (action)</th>
							<td>
								<label>
									<input type="checkbox" name="enable_admin_notice" value="1" <?php checked(1, (int) $settings['enable_admin_notice']); ?> />
									Afficher un bandeau dans l’admin via <code>admin_notices</code>
								</label>
							</td>
						</tr>

						<tr>
							<th scope="row">Extraits (filtres)</th>
							<td>
								<label>
									<input type="checkbox" name="enable_excerpt_tweaks" value="1" <?php checked(1, (int) $settings['enable_excerpt_tweaks']); ?> />
									Modifier <code>excerpt_length</code> et <code>excerpt_more</code>
								</label>
							</td>
						</tr>

						<tr>
							<th scope="row">Badge dans le contenu (filtre)</th>
							<td>
								<label>
									<input type="checkbox" name="enable_content_badge" value="1" <?php checked(1, (int) $settings['enable_content_badge']); ?> />
									Ajouter un badge en fin de contenu via <code>the_content</code>
								</label>
							</td>
						</tr>

						<tr>
							<th scope="row">Requête home (action)</th>
							<td>
								<label>
									<input type="checkbox" name="enable_home_query" value="1" <?php checked(1, (int) $settings['enable_home_query']); ?> />
									Ajuster la requête principale sur la home via <code>pre_get_posts</code>
								</label>
								<p class="description">
									Désactivé par défaut : c’est le genre de hook qui crée des surprises si vous avez une page builder qui remplace la loop.
								</p>
							</td>
						</tr>
					</tbody>
				</table>

				<p>
					<input type="submit" class="button button-primary" name="bpcab_hooks_lab_submit" value="Enregistrer" />
				</p>
			</form>
		</div>
		<?php
	}

	public static function maybe_save_settings(): void {
		if (!is_admin()) {
			return;
		}

		if (!isset($_POST['bpcab_hooks_lab_submit'])) {
			return;
		}

		if (!current_user_can('manage_options')) {
			return;
		}

		// Protection CSRF.
		if (!isset($_POST['bpcab_hooks_lab_nonce']) || !check_admin_referer('bpcab_hooks_lab_save', 'bpcab_hooks_lab_nonce')) {
			return;
		}

		$new = [
			'enable_admin_notice'   => isset($_POST['enable_admin_notice']) ? 1 : 0,
			'enable_excerpt_tweaks' => isset($_POST['enable_excerpt_tweaks']) ? 1 : 0,
			'enable_content_badge'  => isset($_POST['enable_content_badge']) ? 1 : 0,
			'enable_home_query'     => isset($_POST['enable_home_query']) ? 1 : 0,
		];

		update_option(self::OPTION_KEY, $new, false);

		// Redirection pour éviter le resubmit du formulaire.
		wp_safe_redirect(
			add_query_arg(
				['page' => 'bpcab-hooks-lab', 'updated' => '1'],
				admin_url('options-general.php')
			)
		);
		exit;
	}

	public static function maybe_print_admin_notice(): void {
		$settings = self::get_settings();
		if ((int) $settings['enable_admin_notice'] !== 1) {
			return;
		}

		if (!current_user_can('manage_options')) {
			return;
		}

		// Action = on produit un effet (HTML), pas de valeur à retourner.
		echo '<div class="notice notice-info is-dismissible">';
		echo '<p><strong>Hooks Lab</strong> : l’avis admin est affiché via <code>admin_notices</code> (action).</p>';
		echo '</div>';
	}

	public static function filter_excerpt_length(int $length): int {
		$settings = self::get_settings();
		if ((int) $settings['enable_excerpt_tweaks'] !== 1) {
			return $length;
		}

		// Filtre = on transforme une valeur et on la retourne.
		// Attention : ce filtre s'applique surtout à the_excerpt() et aux thèmes classiques.
		return 22;
	}

	public static function filter_excerpt_more(string $more): string {
		$settings = self::get_settings();
		if ((int) $settings['enable_excerpt_tweaks'] !== 1) {
			return $more;
		}

		// Gardez ça simple : pas de HTML lourd ici, ça s'applique à beaucoup d'endroits.
		return '…';
	}

	public static function filter_the_content_append_badge(string $content): string {
		$settings = self::get_settings();
		if ((int) $settings['enable_content_badge'] !== 1) {
			return $content;
		}

		// Évitez d'altérer l'admin, le RSS, et les requêtes REST.
		if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
			return $content;
		}

		// Ne modifiez que le contenu principal d'un post, pas les widgets/shortcodes qui appellent the_content en cascade.
		if (!is_singular('post') || !in_the_loop() || !is_main_query()) {
			return $content;
		}

		$badge = '<div class="bpcab-content-badge" style="margin-top:1.5em;padding:.75em 1em;border:1px solid #e5e7eb;border-radius:10px;background:#f8fafc">'
		       . '<strong>Note</strong> : ce badge est ajouté via le filtre <code>the_content</code>.'
		       . '</div>';

		return $content . $badge;
	}

	public static function action_pre_get_posts_adjust_home_query(WP_Query $query): void {
		$settings = self::get_settings();
		if ((int) $settings['enable_home_query'] !== 1) {
			return;
		}

		// pre_get_posts est une action : on modifie l'objet $query (effet de bord), rien à retourner.
		if (is_admin() || !$query->is_main_query()) {
			return;
		}

		// Exemple : sur la home (liste d'articles), exclure une catégorie "Sponsorisé" (slug: sponsored).
		if ($query->is_home()) {
			$cat = get_category_by_slug('sponsored');
			if ($cat instanceof WP_Term) {
				$query->set('cat', '-' . (string) $cat->term_id);
			}

			// Exemple : limiter à 8 posts.
			$query->set('posts_per_page', 8);
		}
	}
}

BPCAB_Hooks_Lab::boot();

Explication du code

Action vs filtre : la règle opérationnelle

  • Action : votre callback fait quelque chose (log, enqueue, echo, modification d’un objet global, envoi d’email). Le retour est ignoré.
  • Filtre : votre callback reçoit une valeur, la modifie, et doit la retourner. Si vous ne retournez rien, WordPress considère null et vous cassez la chaîne de filtres.

“Twist” utile : add_action() et add_filter() utilisent le même moteur

Dans le cœur, add_action() est un alias de add_filter(). C’est documenté et visible dans le code source du core sur GitHub.

Ce que ça change pour vous : WordPress ne vous “protège” pas. Si vous accrochez une action à un filtre, ou l’inverse, il n’y aura pas forcément d’erreur. Vous devez être strict sur la signature et le retour.

Pourquoi un mini-plugin avec une classe statique

  • Le code est chargé de façon prévisible (au chargement des plugins).
  • Pas de pollution du namespace global.
  • Vous pouvez désactiver le plugin pour isoler un bug (beaucoup plus simple qu’un functions.php).

Permissions + nonce sur la page de réglages

Le trio classique :

  • current_user_can('manage_options') : empêche un contributeur de modifier vos réglages.
  • wp_nonce_field() + check_admin_referer() : bloque les soumissions CSRF.
  • wp_safe_redirect() : évite le double-submit au rafraîchissement.

Le filtre the_content et ses garde-fous

Le problème vient de l’écosystème : the_content peut être appelé dans des contextes inattendus (builder, widgets, previews, REST). Sans garde-fous, vous ajoutez votre badge partout, y compris dans un email ou un flux RSS.

  • is_singular('post') limite aux articles.
  • in_the_loop() + is_main_query() évite beaucoup de doublons.
  • REST_REQUEST et wp_doing_ajax() évitent de casser des réponses machine-to-machine.

pre_get_posts : action “dangereuse” mais puissante

pre_get_posts est une action parce qu’on modifie l’objet WP_Query (effet de bord). C’est puissant, mais c’est aussi un des hooks qui crée le plus de surprises sur des sites Elementor/Divi/Avada, car ces builders peuvent remplacer la loop ou injecter leurs propres requêtes.

Variantes et cas d’usage

Variante 1 — Utiliser un filtre pour modifier un argument plutôt que “echo”

Cas réel : vous voulez changer l’URL d’un avatar Gravatar ou forcer une taille. C’est un filtre, pas une action.

<?php
add_filter('get_avatar_data', function (array $args, $id_or_email) : array {
	// Exemple : forcer la taille à 64 partout (à utiliser avec prudence).
	$args['size'] = 64;
	return $args;
}, 20, 2);

Variante 2 — Priorités : résoudre un conflit avec un plugin

Si un plugin modifie déjà excerpt_length à la priorité 10 (valeur par défaut), placez-vous après (20) pour “gagner”. À l’inverse, placez-vous avant (5) si vous voulez être sur la base.

<?php
add_filter('excerpt_length', function (int $length): int {
	return 30;
}, 99); // 99 = vous passez après la plupart des plugins

Variante 3 — Retirer un hook existant (le cas “je ne contrôle pas le plugin”)

Vous ne pouvez retirer un hook que si vous connaissez la même callback et la même priorité. C’est une source classique de frustration.

<?php
// Exemple : retirer une callback d'un plugin (fictif) à une action.
add_action('init', function () {
	// ⚠️ Vous devez connaître la classe/méthode exacte et la priorité.
	remove_action('wp_head', ['Plugin_Class', 'print_meta'], 10);
});

Si la callback est une closure anonyme, vous ne pourrez pas la retirer facilement. Dans ce cas, cherchez un hook alternatif ou surchargez le comportement via un filtre plus tardif.

Compatibilité Divi 5 / Elementor / Avada

Divi 5

Divi peut réutiliser the_content dans des modules, et parfois hors loop. Votre garde-fou in_the_loop() + is_main_query() évite que le badge se répète dans des layouts globaux.

  • Si vous voulez afficher le badge dans un module Divi, préférez un shortcode (puis insérez-le dans le module Texte/Code) plutôt que de filtrer tout le contenu.
<?php
add_shortcode('hooks_lab_badge', function (): string {
	return '<div class="bpcab-content-badge">Badge via shortcode.</div>';
});

Elementor

Elementor utilise beaucoup de templates et de rendu dynamique. Si vous filtrez the_content, testez aussi :

  • une page “Elementor Canvas”,
  • un template de single,
  • un listing (Loop/Grid) qui peut ne pas appeler the_excerpt() de façon standard.

Pour des ajustements ciblés Elementor, la voie la plus stable est souvent un widget custom ou un shortcode, plutôt qu’un filtre global.

Avada (Fusion Builder)

Avada a ses propres shortcodes et composants. Les filtres d’extrait peuvent ne pas impacter certains éléments (car Avada peut générer des résumés autrement). Dans ce cas :

  • gardez vos filtres, mais acceptez qu’ils ne couvrent pas 100% des rendus du builder,
  • ou adaptez via les options Avada / hooks spécifiques Avada si disponibles (selon version).

Vérifications après mise en place

  • Activez le plugin, puis allez dans Réglages → Hooks Lab.
  • Cochez “Avis admin” : vous devez voir un bandeau info dans l’admin.
  • Cochez “Badge dans le contenu” : ouvrez un article (single) côté front, le badge doit apparaître une seule fois.
  • Cochez “Extraits” : vérifiez une page d’archives/blog qui affiche des extraits.
  • Si vous activez “Requête home” : la home doit passer à 8 articles et exclure la catégorie sponsored si elle existe.

Tableau de diagnostic (symptômes fréquents)

Symptôme Cause probable Vérification Solution
Le filtre ne “fait rien” Mauvais hook, mauvaise priorité, ou le thème/builder n’utilise pas cette API Avec Query Monitor, cherchez si le hook est déclenché et si votre callback est attachée Changer de hook, ajuster la priorité, ou passer par shortcode/widget builder
Le site affiche une page blanche (500) Erreur PHP (point-virgule manquant, parenthèse, version PHP trop ancienne) Consultez les logs PHP / wp-content/debug.log en staging Corriger la syntaxe, vérifier PHP 8.1+, désactiver le plugin via FTP si besoin
Le badge apparaît plusieurs fois the_content appelé hors loop ou plusieurs fois par le thème/builder Vérifiez in_the_loop() et is_main_query(), testez avec un thème par défaut Renforcer les garde-fous, ou passer à un shortcode inséré manuellement
Rien ne change après activation Cache plugin/serveur/CDN Désactivez temporairement cache, purge CDN, test en navigation privée Purger tous les caches et re-tester
La home ne respecte plus la pagination Modification agressive de pre_get_posts Testez page 2/3, vérifiez paramètres paged Limiter la modification à $query->is_home() et ne pas toucher à paged

Si ça ne marche pas

1) Vérifiez que le code est au bon endroit

  • Si vous avez collé dans functions.php mais que vous avez changé de thème : votre code ne charge plus.
  • Si vous avez collé dans un plugin de snippets : vérifiez qu’il est activé et qu’il n’exécute pas en “admin only”.

2) Vérifiez la signature de la callback

Erreur classique : vous déclarez 2 paramètres, mais vous n’avez pas mis $accepted_args dans add_filter().

<?php
// ✅ Correct : 2 paramètres, donc accepted_args = 2
add_filter('get_avatar_data', 'ma_cb', 10, 2);

function ma_cb(array $args, $id_or_email): array {
	return $args;
}

3) Vérifiez la priorité

  • Si un plugin écrase votre valeur après vous : augmentez la priorité (ex: 99).
  • Si vous dépendez d’une valeur “déjà modifiée” par un autre plugin : placez-vous après lui.

4) Vérifiez que le hook est réellement déclenché

Deux fonctions très pratiques pour raisonner :

  • did_action('hook') : combien de fois l’action a été déclenchée.
  • doing_filter('hook') : êtes-vous en train d’exécuter ce filtre.
<?php
add_action('wp_footer', function () {
	if (current_user_can('manage_options')) {
		echo '<!-- did_action(the_content): ' . (int) did_action('the_content') . ' -->';
	}
});

5) Éliminez les interférences (thème/builder)

  • Testez temporairement avec un thème par défaut (en staging).
  • Désactivez les plugins de cache.
  • Avec Elementor/Divi/Avada, testez une page “simple” sans template complexe.

Pièges et erreurs courantes

Erreur Cause Solution
Utiliser add_action('the_content', ...) puis oublier de retourner the_content est un filtre. Sans retour, le contenu devient vide Utilisez add_filter et retournez toujours $content
Warning: Cannot modify header information - headers already sent Un echo dans un filtre exécuté avant l’envoi des headers (ou BOM/espaces dans un fichier) Supprimez les echo dans les filtres, vérifiez les espaces avant <?php
Hook “ne se déclenche pas” Vous êtes sur le mauvais hook (trop tôt/trop tard) ou sur un template builder qui ne l’appelle pas Confirmez avec Query Monitor, changez de hook ou utilisez shortcode/widget
Fatal error après collage du code Parenthèse/point-virgule manquant, ou PHP < 8.1 Corrigez la syntaxe, vérifiez la version PHP, désactivez via FTP si nécessaire
Conflit de priorité Deux plugins filtrent la même valeur Ajustez la priorité, ou retirez l’autre hook si possible
Changements invisibles Cache navigateur/serveur/CDN Purger tous les caches, tester en navigation privée
Le code marche en admin mais pas en front (ou inverse) Hook accroché dans un contexte non exécuté Vérifiez is_admin(), utilisez le bon hook front (wp_enqueue_scripts, wp_head, etc.)

Conseils sécurité, performance et maintenance

  • Sécurité : dès que vous enregistrez des réglages, utilisez current_user_can() + nonce. Sans ça, vous ouvrez la porte à des modifications non autorisées via CSRF.
  • Évitez les filtres “lourds” : the_content peut s’exécuter très souvent. Ne faites pas de requêtes SQL dedans, ne chargez pas de fichiers, ne faites pas d’appels HTTP.
  • Escaping : pour du HTML fixe, vous pouvez concaténer. Pour toute donnée dynamique, échappez avec esc_html(), esc_attr(), esc_url() selon le contexte.
  • Priorités : documentez vos priorités. Quand vous mettez 99, mettez un commentaire “pour passer après tel plugin” — sinon c’est incompréhensible plus tard.
  • Maintenance : regroupez vos hooks dans une classe (ou plusieurs) et évitez les closures anonymes si vous pensez devoir retirer le hook un jour.
  • Compatibilité future : ciblez PHP 8.1+ (typage strict ok), et évitez les patterns d’anciens tutos (ex: écrire directement dans wp-config.php en prod, ou faire du SQL direct dans un filtre).

Ressources

FAQ

Est-ce que add_action() et add_filter() sont vraiment différents ?

Conceptuellement oui (effet de bord vs transformation d’une valeur). Techniquement, ils reposent sur la même API interne. La différence pratique, c’est que un filtre doit retourner une valeur, une action non.

Pourquoi mon filtre marche avec add_action() parfois ?

Parce que WordPress ne vous empêche pas de vous accrocher. Si votre callback retourne une valeur “par hasard”, ça peut fonctionner. Le jour où vous modifiez la callback (ou qu’un autre plugin change l’ordre), ça casse. Gardez la discipline : filtre → add_filter.

Comment savoir combien d’arguments mon hook reçoit ?

Regardez la documentation du hook (developer.wordpress.org) ou le code source où le hook est appliqué. Pour un filtre, cherchez apply_filters('hook', $value, ...). Pour une action, cherchez do_action('hook', ...).

Que se passe-t-il si je ne retourne rien dans un filtre ?

Votre callback retourne null. Le filtre suivant reçoit null au lieu de la valeur attendue. Sur the_content, ça peut vider complètement le contenu.

Quel est le meilleur endroit pour mettre mes hooks ?

Pour des modifications “site” (pas liées au thème), un mini-plugin est souvent plus maintenable. Pour des modifications strictement liées au rendu d’un thème, un thème enfant est acceptable.

Comment gérer un conflit de hooks entre deux plugins ?

Commencez par ajuster la priorité. Si ce n’est pas suffisant, retirez le hook concurrent avec remove_action/remove_filter (si la callback est retirable). Sinon, cherchez un hook plus tardif, ou un hook différent qui s’applique après.

Pourquoi pre_get_posts est une action et pas un filtre ?

Parce que vous modifiez l’objet WP_Query par effet de bord. Il n’y a pas de “valeur” à retourner : vous faites des $query->set().

Est-ce que je peux “echo” dans une action ?

Oui, si l’action est exécutée dans un contexte de rendu HTML (ex: wp_footer, admin_notices). Ne faites pas ça dans des actions qui peuvent se déclencher pendant des réponses JSON/REST.

Pourquoi mes changements ne se voient pas avec Elementor/Divi/Avada ?

Parce que le builder peut ne pas utiliser the_excerpt() ou peut rendre du contenu via ses propres templates. Dans ce cas, un shortcode ou un widget custom est souvent plus fiable qu’un filtre global.

Comment tester proprement sans casser la prod ?

Staging + Query Monitor + logs. Activez/désactivez vos hooks via une option (comme dans le plugin ci-dessus) et purgez les caches. Si vous devez tester en prod, faites-le sur un créneau, avec sauvegarde et possibilité de rollback rapide.

Un ancien tutoriel me dit de mettre ça dans functions.php. C’est “mauvais” ?

Ce n’est pas mauvais, mais c’est fragile. Sur des sites qui changent de thème (ou qui utilisent un builder), vous perdez vos hooks. En 2026, je privilégie un mini-plugin pour tout ce qui n’est pas purement “thème”.