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->préparé(
"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)
Création de site web sur mesure
Pack inclus : conception professionnelle, domaine et hébergement la première année, formation, support 6 mois, accès et code livrés.
À partir de 350 000 FCFA
Workflow Git pour WordPress en 2026
Un projet WordPress professionnel n est pas heberge directement en production : il vit dans un depot Git, les modifications passent par des branches, et un workflow CI/CD deploie en staging puis en production. Cette discipline distingue le travail amateur du travail professionnel. Sans Git, impossible de revenir en arriere apres une erreur, impossible de collaborer a plusieurs developpeurs, impossible d auditer qui a fait quoi quand.
Le pattern typique : depot contenant uniquement le code du theme custom et des plugins custom. Le core WordPress, WooCommerce et les plugins tiers s installent via composer ou WP-CLI a la mise en place de chaque environnement. Le fichier .gitignore exclut wp-content/uploads, wp-content/cache, wp-content/upgrade, et le wp-config.php local.
Separation environnements : local, staging, production
Trois environnements minimum sont necessaires. Local : sur la machine du developpeur, avec LocalWP, DevKinsta, ou Docker. Permet d experimenter sans risque. Staging : un clone de production accessible aux clients pour validation. Heberge typiquement sur un sous-domaine ou un mot de passe HTTP basique. Production : le site live, modifie uniquement apres validation en staging.
Le piege classique : faire les modifications directement en production. Toute base de code professionnelle interdit ce pattern. Si vraiment necessaire pour un bugfix urgent, la modification doit etre commitee en Git et re-deployee proprement dans la foulee, jamais laissee comme un patch divergent.
Normes de codage WordPress (PHPCS)
WordPress maintient un ensemble de standards de code (WordPress Coding Standards) qui couvre PHP, JS, CSS, et HTML. L outil PHPCS (PHP_CodeSniffer) avec le ruleset WordPress detecte automatiquement les ecarts de style et les vulnerabilites classiques (XSS, SQL injection, fonctions non echappees).
Installation : composer require --dev wp-coding-standards/wpcs squizlabs/php_codesniffer dans le depot. Configuration : un fichier phpcs.xml a la racine qui pointe sur les fichiers a verifier et exclut ceux qui ne le sont pas (wp-content/uploads, vendor, node_modules). Execution : ./vendor/bin/phpcs en CLI ou via une integration VS Code.
L integration dans la CI/CD garantit qu aucun commit ne casse le standard. Un GitHub Actions workflow de 20 lignes execute PHPCS a chaque pull request et bloque le merge en cas d echec.
Tests automatises pour WordPress
Trois niveaux de tests structurent un projet serieux.
Tests unitaires PHP avec PHPUnit + WP_UnitTestCase. Verifient le comportement de fonctions et de classes isolees. Cible : la logique metier critique (calculs, validations, transformations de donnees). 100 a 500 tests pour un theme complexe.
Tests d integration WordPress avec WP Browser (base sur Codeception). Verifient les comportements end-to-end : creer un post, le publier, naviguer sur le front, soumettre un formulaire. Cible : les workflows utilisateur principaux.
Tests E2E navigateur avec Playwright ou Cypress. Verifient le rendu visuel et le comportement JavaScript du front. Cible : les tunnels critiques (checkout WooCommerce, formulaire de contact, login). 5 a 20 tests E2E sont generalement suffisants.
Securite — discipline non negociable
Quatre regles fondamentales structurent un code WordPress sur :
Echapper toute sortie. Toute variable affichee dans le HTML passe par esc_html(), esc_attr(), esc_url(), ou wp_kses_post(). Une regression sur cette discipline ouvre des failles XSS. PHPCS detecte automatiquement les manquements.
Sanitizer toute entree. Les variables $_GET, $_POST, $_REQUEST sont passees par sanitize_text_field(), absint(), sanitize_email() avant utilisation. Pour les SQL, jamais d interpolation directe : utiliser $wpdb->prepare() avec parametres.
Verifier les nonces. Tout formulaire qui modifie quelque chose inclut wp_nonce_field() et le traitement appelle wp_verify_nonce(). Sans nonce, les attaques CSRF deviennent triviales.
Verifier les capabilities. Avant toute action sensible, current_user_can( edit_posts ) ou la capability appropriee. Limite les actions aux utilisateurs autorises.
Performance — quelques regles d or
Pas de WP_Query dans une boucle. Un developpeur novice fait souvent un WP_Query a l interieur d une iteration foreach, ce qui multiplie les requetes SQL. La solution : un seul WP_Query avec include ou post__in pour ramener tous les posts d un coup.
Mise en cache des resultats couteux. Une fonction qui fait un appel API externe ou un calcul lourd est mise en cache avec get_transient / set_transient ou wp_cache_get / wp_cache_set pour la duree appropriee (5 minutes a 24 heures selon le cas).
Lazy loading des images. Toutes les images en dessous de la ligne de flottaison ont l attribut loading= lazy . WordPress 5.5+ l ajoute automatiquement pour les images contenu, mais pas pour les images hardcodees du theme. Verifier dans le code custom.
Minify et concatener. CSS et JS sont minifies et eventuellement concatenes en production via un plugin (WP Rocket, LiteSpeed Cache, Autoptimize). Reduction typique de 30 a 60 pour cent du poids transfere.
FAQ
Faut-il utiliser Composer dans un projet WordPress ?
De plus en plus oui. Pour gerer les dependances PHP (plugins via Roots/wpackagist, libraries via Packagist), Composer est devenu standard. Le pattern Bedrock (roots.io/bedrock) structure un projet WordPress complet autour de Composer et est largement adopte par les agences serieuses.
Quel hebergeur pour la production ?
Pour les projets serieux : WP Engine, Kinsta, Pressable (managed WordPress haut de gamme, 30-200 USD/mois). Pour les budgets serres avec maitrise serveur : Hostinger VPS, OVH VPS, DigitalOcean Droplet (10-50 USD/mois). Eviter les mutualises bas de gamme qui partagent les ressources avec des centaines de sites.
Comment migrer un site WordPress entre environnements ?
WP Migrate (delicious-brains), Duplicator, ou WP-CLI custom script. Toujours tester la migration sur un sous-domaine staging avant de basculer la prod. Verifier les permaliens, les URLs en base (search-replace), et les fichiers media.
Combien de plugins maximum sur un site WordPress ?
Pas de limite absolue. La regle empirique : 20-30 plugins actifs sur un site standard, 50+ sur un e-commerce complexe. Chaque plugin ajoute du code execute a chaque requete. Auditer regulierement les plugins inutilises et les desactiver. Eviter les bundles plugins multipurpose qui ajoutent 80 pour cent de fonctionnalites inutilisees.
References
- WordPress Coding Standards — developer.wordpress.org/coding-standards
- WordPress PHP Coding Standards (PHPCS) — github.com/WordPress/WordPress-Coding-Standards
- Bedrock (Roots) — roots.io/bedrock
- WP Browser (Codeception) — wpbrowser.wptestkit.dev
- WP-CLI — wp-cli.org
- Plugin Boilerplate — wppb.io