Pourquoi les bonnes pratiques de développement WordPress importent
WordPress propulse plus de 40% du web mondial, mais la majorité des sites WordPress sont mal codés. Thèmes bricolés, plugins qui se marchent dessus, functions.php de 2000 lignes, mises à jour qui cassent tout — c’est le quotidien de beaucoup de développeurs WordPress au Sénégal et ailleurs. Ce guide couvre les pratiques professionnelles qui font la différence entre un site maintenable et un cauchemar technique.
Toujours utiliser un thème enfant
C’est la règle n°1, non négociable. Si vous modifiez directement un thème parent, vos modifications seront écrasées à la prochaine mise à jour du thème.
Créer un thème enfant
Structure minimale dans wp-content/themes/montheme-child/ :
montheme-child/
├── style.css
├── functions.php
└── screenshot.png (optionnel)
style.css du thème enfant
/*
Theme Name: MonThème Child
Template: montheme-parent
Description: Thème enfant pour personnalisations
Version: 1.0
*/
/* Vos styles personnalisés ici */
La ligne Template doit correspondre EXACTEMENT au nom du dossier du thème parent.
functions.php du thème enfant
<?php
// Charger les styles du thème parent puis du thème enfant
function montheme_child_enqueue_styles() {
wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css');
wp_enqueue_style('child-style', get_stylesheet_uri(), array('parent-style'));
}
add_action('wp_enqueue_scripts', 'montheme_child_enqueue_styles');
Toutes vos modifications de template (header.php, footer.php, single.php, etc.) vont dans le thème enfant. WordPress les charge en priorité sur ceux du parent.
Charger correctement les scripts et styles
Ne jamais insérer des balises <script> ou <link> directement dans le HTML. Utilisez le système d’enqueue de WordPress :
La bonne méthode
function itsc_enqueue_assets() {
// CSS
wp_enqueue_style(
'itsc-custom', // Handle unique
get_stylesheet_directory_uri() . '/css/custom.css', // Chemin
array(), // Dépendances
filemtime(get_stylesheet_directory() . '/css/custom.css') // Version = date modif
);
// JavaScript (dans le footer)
wp_enqueue_script(
'itsc-main',
get_stylesheet_directory_uri() . '/js/main.js',
array('jquery'), // Dépendances
filemtime(get_stylesheet_directory() . '/js/main.js'),
true // true = charger dans le footer
);
// Passer des données PHP à JavaScript
wp_localize_script('itsc-main', 'itscData', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('itsc_nonce'),
'siteUrl' => home_url()
));
}
add_action('wp_enqueue_scripts', 'itsc_enqueue_assets');
Pourquoi c’est important
- Pas de doublons : WordPress ne charge pas deux fois le même handle
- Gestion des dépendances : jQuery est chargé avant votre script qui en dépend
- Cache busting automatique : Le paramètre version force le rechargement quand le fichier change
- Compatibilité plugins : Les plugins de cache et de minification peuvent optimiser les fichiers enqueued
Sécuriser votre code WordPress
Valider et assainir les entrées utilisateur
Ne faites JAMAIS confiance aux données envoyées par l’utilisateur :
// MAUVAIS : injection SQL possible
$results = $wpdb->get_results(
"SELECT * FROM wp_posts WHERE post_title = '" . $_GET['search'] . "'"
);
// BON : requête préparée
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
sanitize_text_field($_GET['search'])
)
);
Fonctions de sanitization WordPress
// Texte simple (pas de HTML)
$clean = sanitize_text_field($_POST['name']);
// Email
$email = sanitize_email($_POST['email']);
// URL
$url = esc_url($_POST['website']);
// HTML limité (garde les balises autorisées)
$html = wp_kses_post($_POST['content']);
// Entier
$id = absint($_POST['post_id']);
// Nom de fichier
$file = sanitize_file_name($_POST['filename']);
Échapper les sorties
Avant d’afficher une donnée dynamique dans le HTML, échappez-la :
// Dans le HTML
<h1><?php echo esc_html($titre); ?></h1>
// Dans un attribut HTML
<a href="<?php echo esc_url($lien); ?>">Lien</a>
// Dans un attribut général
<div data-id="<?php echo esc_attr($id); ?>">
// JavaScript inline
<script>var data = <?php echo wp_json_encode($data); ?>;</script>
Vérifier les nonces (CSRF protection)
// Dans le formulaire
wp_nonce_field('itsc_save_action', 'itsc_nonce');
// Lors du traitement
if (!isset($_POST['itsc_nonce']) ||
!wp_verify_nonce($_POST['itsc_nonce'], 'itsc_save_action')) {
wp_die('Action non autorisée');
}
Utiliser les hooks correctement
Les hooks (actions et filtres) sont le cœur de WordPress. Comprendre la différence :
Actions : faire quelque chose à un moment précis
// Ajouter du contenu après chaque article
function itsc_after_content($content) {
if (is_single() && in_the_loop() && is_main_query()) {
$cta = '<div class="post-cta">'
. '<p>Vous avez trouvé cet article utile ?</p>'
. '<a href="/newsletter">Abonnez-vous à notre newsletter</a>'
. '</div>';
$content .= $cta;
}
return $content;
}
add_filter('the_content', 'itsc_after_content');
Filtres : modifier une valeur avant qu’elle soit utilisée
// Modifier la longueur de l'extrait
function itsc_excerpt_length($length) {
return 30; // 30 mots au lieu de 55 par défaut
}
add_filter('excerpt_length', 'itsc_excerpt_length');
// Modifier le texte "Lire la suite"
function itsc_excerpt_more($more) {
return '... <a href="' . get_permalink() . '">Lire la suite →</a>';
}
add_filter('excerpt_more', 'itsc_excerpt_more');
Priorité et nombre d’arguments
// add_filter(hook, callback, priorité, nb_arguments)
add_filter('the_title', 'ma_fonction', 10, 2);
function ma_fonction($title, $post_id) {
// priorité 10 = exécution par défaut
// priorité 1 = exécuté en premier
// priorité 99 = exécuté en dernier
return $title;
}
Requêtes personnalisées avec WP_Query
N’utilisez jamais de requêtes SQL directes quand WP_Query peut faire le travail :
// Afficher les 6 derniers articles d'une catégorie
$query = new WP_Query(array(
'post_type' => 'post',
'posts_per_page' => 6,
'category_name' => 'wordpress',
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
// Optimisation : ne pas compter le total si on ne pagine pas
'no_found_rows' => true,
// Ne charger que ce dont on a besoin
'fields' => 'ids', // Seulement les IDs
));
if ($query->have_posts()) :
while ($query->have_posts()) : $query->the_post();
// Affichage
echo '<h3>' . get_the_title() . '</h3>';
endwhile;
wp_reset_postdata(); // TOUJOURS réinitialiser après une custom query
endif;
Optimiser les requêtes
// MAUVAIS : charge tout en mémoire
$query = new WP_Query(array(
'posts_per_page' => -1, // TOUS les posts = lent
));
// BON : limiter et paginer
$query = new WP_Query(array(
'posts_per_page' => 12,
'paged' => get_query_var('paged') ?: 1,
'no_found_rows' => false, // Nécessaire pour la pagination
));
// BON : ne charger que les champs nécessaires
$query = new WP_Query(array(
'posts_per_page' => 10,
'no_found_rows' => true,
'update_post_meta_cache' => false, // Pas besoin des meta
'update_post_term_cache' => false, // Pas besoin des termes
));
Custom Post Types et Taxonomies
Quand les articles et pages ne suffisent pas, créez des types personnalisés :
// Exemple : type "Portfolio"
function itsc_register_portfolio() {
register_post_type('portfolio', array(
'labels' => array(
'name' => 'Projets',
'singular_name' => 'Projet',
'add_new_item' => 'Ajouter un projet',
'edit_item' => 'Modifier le projet',
),
'public' => true,
'has_archive' => true,
'rewrite' => array('slug' => 'projets'),
'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
'menu_icon' => 'dashicons-portfolio',
'show_in_rest' => true, // Support Gutenberg
));
// Taxonomie personnalisée
register_taxonomy('type_projet', 'portfolio', array(
'labels' => array(
'name' => 'Types de projet',
'singular_name' => 'Type',
),
'hierarchical' => true, // Comme les catégories
'rewrite' => array('slug' => 'type-projet'),
'show_in_rest' => true,
));
}
add_action('init', 'itsc_register_portfolio');
N’oubliez pas : Après avoir créé un CPT, allez dans Réglages → Permaliens et cliquez « Enregistrer » (sans rien changer) pour régénérer les règles de réécriture.
AJAX dans WordPress
Pour les interactions dynamiques sans rechargement de page :
Côté PHP (functions.php)
// Handler AJAX (connecté + non connecté)
function itsc_load_more_posts() {
// Vérifier le nonce
check_ajax_referer('itsc_nonce', 'nonce');
$page = absint($_POST['page']);
$query = new WP_Query(array(
'post_type' => 'post',
'posts_per_page' => 6,
'paged' => $page,
'post_status' => 'publish',
));
$html = '';
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$html .= '<article class="post-card">';
$html .= '<h3>' . get_the_title() . '</h3>';
$html .= '<p>' . get_the_excerpt() . '</p>';
$html .= '</article>';
}
wp_reset_postdata();
}
wp_send_json_success(array(
'html' => $html,
'hasMore' => $page < $query->max_num_pages,
));
}
add_action('wp_ajax_load_more', 'itsc_load_more_posts');
add_action('wp_ajax_nopriv_load_more', 'itsc_load_more_posts');
Côté JavaScript
document.querySelector('.load-more-btn').addEventListener('click', async function() {
const btn = this;
const page = parseInt(btn.dataset.page) + 1;
btn.textContent = 'Chargement...';
const formData = new FormData();
formData.append('action', 'load_more');
formData.append('page', page);
formData.append('nonce', itscData.nonce);
const response = await fetch(itscData.ajaxUrl, {
method: 'POST',
body: formData,
});
const data = await response.json();
if (data.success) {
document.querySelector('.posts-grid').insertAdjacentHTML('beforeend', data.data.html);
btn.dataset.page = page;
btn.textContent = 'Charger plus';
if (!data.data.hasMore) {
btn.remove();
}
}
});
Gestion des erreurs et débogage
wp-config.php en développement
// Activer le mode debug (JAMAIS en production)
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true); // Écrit les erreurs dans wp-content/debug.log
define('WP_DEBUG_DISPLAY', false); // Ne pas afficher les erreurs à l'écran
define('SCRIPT_DEBUG', true); // Utiliser les fichiers CSS/JS non minifiés
wp-config.php en production
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', false);
Logger proprement
// Fonction de log réutilisable
function itsc_log($message, $data = null) {
if (WP_DEBUG_LOG) {
$log = '[ITSC ' . current_time('Y-m-d H:i:s') . '] ' . $message;
if ($data !== null) {
$log .= ' | Data: ' . print_r($data, true);
}
error_log($log);
}
}
// Usage
itsc_log('Formulaire soumis', $_POST);
itsc_log('Erreur de paiement pour commande #' . $order_id);
Organisation du code
Quand votre functions.php dépasse 200 lignes, découpez-le :
// functions.php — point d'entrée, n'inclut que les fichiers
require_once get_stylesheet_directory() . '/inc/setup.php'; // Configuration du thème
require_once get_stylesheet_directory() . '/inc/enqueue.php'; // Scripts et styles
require_once get_stylesheet_directory() . '/inc/custom-post-types.php'; // CPT
require_once get_stylesheet_directory() . '/inc/shortcodes.php'; // Shortcodes
require_once get_stylesheet_directory() . '/inc/ajax-handlers.php'; // AJAX
require_once get_stylesheet_directory() . '/inc/helpers.php'; // Fonctions utilitaires
Checklist avant mise en production
- ☐ Thème enfant utilisé (pas de modification du thème parent)
- ☐ Tous les scripts/styles chargés via wp_enqueue
- ☐ Toutes les entrées utilisateur sanitisées
- ☐ Toutes les sorties échappées (esc_html, esc_attr, esc_url)
- ☐ Nonces vérifiés sur tous les formulaires et requêtes AJAX
- ☐ WP_DEBUG désactivé en production
- ☐ Pas de requêtes SQL directes (utiliser WP_Query, get_posts)
- ☐ wp_reset_postdata() après chaque custom query
- ☐ Pas de functions.php de plus de 200 lignes (découper en fichiers)
- ☐ Préfixe unique sur toutes vos fonctions (éviter les conflits)