WP_Query est la classe WordPress qui récupère les articles. Les développeurs de thèmes s’en servent chaque fois qu’une page a besoin d’une liste que l’adresse ne fournit pas déjà, comme des articles connexes ou un bandeau de nouvelles. Elle travaille avec la Boucle (The Loop), le court bloc while ( have_posts() ) présent dans chaque gabarit de thème classique, qui affiche les articles que WordPress a déjà récupérés pour l’adresse courante. Les ennuis commencent quand on greffe une deuxième liste : la barre latérale affiche le mauvais titre, la page 2 de l’archive renvoie une erreur 404 ou la page ralentit. La solution a deux volets. Utilisez new WP_Query suivi de wp_reset_postdata() pour une liste supplémentaire, et le crochet pre_get_posts pour modifier la liste principale d’une adresse. Évitez query_posts() dans les deux cas.
Ce que vous allez accomplir
- Voir exactement ce que la requête principale a demandé à la base de données pour n’importe quelle adresse, afin de modifier la bonne chose.
- Écrire une boucle secondaire (un bloc d’articles connexes, un bandeau de nouvelles) qui ne peut pas déborder sur le reste du gabarit.
- Modifier la requête principale avec
pre_get_postspour que la pagination, les flux et les balises conditionnelles continuent de fonctionner. - Paginer une requête personnalisée dans un gabarit de page sans erreur 404 à la page 2.
- Reconnaître les cinq erreurs derrière la plupart des signalements du type « ma requête affiche les mauvais articles ».
Prérequis
- WordPress 6.x sur PHP 8.1 ou plus récent. Tout ce qui suit fonctionne aussi sur 5.x, mais les exemples supposent un cœur à jour.
- Un thème enfant ou une petite extension maison pour héberger votre code. Les modifications faites dans un thème parent disparaissent à sa prochaine mise à jour; consultez créer un thème enfant WordPress si vous n’en avez pas encore.
- Une copie de préproduction du site, plus un accès SSH avec WP-CLI (la ligne de commande propre à WordPress) pour lancer des requêtes de test sans charger de pages.
- La journalisation de débogage active en préproduction :
WP_DEBUGetWP_DEBUG_LOGàtruedanswp-config.php, pour que les ligneserror_log()arrivent danswp-content/debug.log. - Si votre thème est un thème de blocs (il contient un fichier
templates/index.html), lisez d’abord la dernière section. Le bloc Boucle de requête fait peut-être déjà ce qu’il vous faut, sans PHP.
Comment ça fonctionne
Chaque chargement de page côté public exécute une requête avant le chargement de tout gabarit. WordPress lit l’adresse, la transforme en variables de requête comme category_name=news ou paged=2, puis exécute un objet WP_Query rangé dans la variable globale $wp_query. C’est la requête principale. Elle décide si la page est un article seul, une archive ou une erreur 404, et la hiérarchie des gabarits choisit un fichier à partir de cette réponse.
La Boucle parcourt ensuite les résultats de la requête principale. have_posts() demande s’il reste un article. the_post() passe à cet article et le copie dans la variable globale $post. Les balises de gabarit comme the_title(), the_permalink() et comments_template() lisent cette variable globale; elles affichent donc toujours « l’article courant ».
Une requête secondaire est tout objet WP_Query que vous créez vous-même. Elle a ses propres résultats et son propre compteur de position. Sa méthode the_post() écrit pourtant dans la même variable globale $post, parce que les balises de gabarit ne connaissent que celle-là. Une fois votre boucle secondaire terminée, $post pointe vers le dernier article de votre liste. Tout ce qui s’exécute plus loin dans le gabarit, comme le formulaire de commentaires ou le code de pied de page d’une extension SEO, lit alors le mauvais article. wp_reset_postdata() recopie l’article courant de la requête principale dans $post et rétablit la page.
Quatre outils touchent à ce mécanisme, et ils ne sont pas interchangeables :
| Outil | Ce qu’il fait | À utiliser pour |
|---|---|---|
new WP_Query( $args ) | Exécute une requête supplémentaire avec ses propres résultats | Une deuxième liste dans la page : articles connexes, bandeau de nouvelles |
get_posts( $args ) | Même moteur, renvoie un simple tableau d’articles | Les listes courtes sans pagination ni balises de gabarit |
Crochet pre_get_posts | Modifie la requête principale avant son exécution | Changer ce que liste une archive, une recherche ou la page du blogue |
query_posts() | Jette la requête principale et la remplace | Rien; elle casse la pagination et les balises conditionnelles |
get_posts() fixe trois valeurs par défaut que WP_Query ne fixe pas : elle ignore les articles épinglés, saute le comptage des lignes qui sert à la pagination et désactive la plupart des filtres de requête avec suppress_filters. C’est donc économique pour une liste des « 3 derniers articles ». Cela veut aussi dire que les extensions qui se branchent sur les filtres de requête, comme les extensions multilingues, peuvent ne pas voir la requête du tout.
Étape par étape
1. Journalisez ce que la requête principale a demandé. Avant d’ajouter une requête, vérifiez ce que la page récupère déjà. Bien des problèmes de « requête personnalisée » se résument à une requête principale dont un seul argument devait changer. Créez wp-content/mu-plugins/sh-query-probe.php en préproduction :
<?php
add_action( 'wp', function () {
global $wp_query;
if ( is_admin() || wp_doing_ajax() ) {
return;
}
error_log( 'main query vars: ' . wp_json_encode( array_filter( $wp_query->query ) ) );
error_log( "main query found {$wp_query->found_posts} posts, {$wp_query->max_num_pages} pages" );
} );
L’action wp se déclenche juste après l’exécution de la requête principale et avant le chargement du gabarit. Demandez une page et lisez le journal :
curl -s -o /dev/null https://staging.example.com/category/news/page/2/
tail -n 2 wp-content/debug.log
[29-Oct-2026 08:14:37 UTC] main query vars: {"category_name":"news","paged":"2"}
[29-Oct-2026 08:14:37 UTC] main query found 47 posts, 5 pages
Exemple de sortie. Si les variables décrivent déjà les articles voulus et que seuls le nombre ou l’ordre clochent, passez à l’étape 4 et modifiez la requête principale. Si vous avez besoin d’une liste distincte à côté de la principale, continuez à l’étape 2.
2. Écrivez une boucle secondaire qui fait le ménage derrière elle. Ce bloc d’articles connexes va dans une partie de gabarit, par exemple template-parts/related.php dans votre thème enfant :
<?php
$related = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 3,
'post__not_in' => array( get_the_ID() ),
'category__in' => wp_get_post_categories( get_the_ID() ),
'ignore_sticky_posts' => true,
'no_found_rows' => true,
) );
if ( $related->have_posts() ) {
echo '<ul class="related-posts">';
while ( $related->have_posts() ) {
$related->the_post();
printf( '<li><a href="%s">%s</a></li>', esc_url( get_permalink() ), esc_html( get_the_title() ) );
}
echo '</ul>';
wp_reset_postdata();
}
Trois arguments font un travail discret mais important. ignore_sticky_posts empêche WordPress d’ajouter les articles épinglés en plus de vos trois. no_found_rows saute le comptage supplémentaire que WordPress fait pour calculer le nombre de pages, inutile pour une liste non paginée. Et wp_reset_postdata() se trouve dans le if, parce que la variable globale $post ne change que si la boucle s’est réellement exécutée. Chargez un article en préproduction : vous devriez voir trois titres en lien, dont aucun n’est l’article que vous lisez.
3. Testez la requête avec WP-CLI avant qu’elle atteigne un gabarit. wp eval exécute du PHP avec WordPress chargé; vous pouvez donc vérifier le SQL et les résultats sans fureteur :
wp eval '
$q = new WP_Query( array( "post_type" => "post", "posts_per_page" => 3 ) );
echo preg_replace( "/\s+/", " ", trim( $q->request ) ), PHP_EOL;
echo "found: {$q->found_posts}, pages: {$q->max_num_pages}", PHP_EOL;
foreach ( $q->posts as $p ) { echo "- {$p->post_title}", PHP_EOL; }
'
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish'))) ORDER BY wp_posts.post_date DESC LIMIT 0, 3
found: 128, pages: 43
- Autumn opening hours
- New invoice portal is live
- Plugin update checklist
Exemple de sortie. $q->request contient le SQL que WordPress a envoyé : c’est le moyen le plus rapide de voir si un argument a été ignoré. Si un argument category__in ou meta_query n’apparaît pas dans la clause WHERE, il était mal orthographié ou du mauvais type. Votre préfixe de tables peut être différent de wp_.
4. Modifiez la requête principale avec pre_get_posts. Pour afficher 6 articles sur la page du blogue et en masquer une catégorie, modifiez la requête principale au lieu d’en ajouter une deuxième. Placez ceci dans le functions.php de votre thème enfant ou dans une extension :
add_action( 'pre_get_posts', function ( WP_Query $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_home() ) {
$query->set( 'posts_per_page', 6 );
$query->set( 'category__not_in', array( 12 ) );
}
} );
Les deux gardes de la première condition ne sont pas facultatives. Sans is_admin(), le changement s’applique aussi aux listes d’articles du tableau de bord. Sans is_main_query(), il s’applique à toutes les requêtes secondaires de la page, y compris celle de l’étape 2. Utilisez les méthodes de $query ($query->is_home()) plutôt que la fonction globale is_home(), pour tester la requête en cours de construction. Trouvez l’identifiant de la catégorie avec wp term list category --fields=term_id,name. Demandez de nouveau la page du blogue. La deuxième ligne de journal de l’étape 1 devrait maintenant montrer un nombre d’articles plus bas, et un nombre de pages égal à ce total divisé par 6, arrondi vers le haut. La première ligne ne mentionnera pas posts_per_page, parce qu’elle journalise les variables tirées de l’adresse, pas celles que votre crochet a modifiées.
5. Paginez une requête personnalisée dans un gabarit de page. Parfois, la liste appartient vraiment à une page, comme une page « Nouvelles » construite avec un gabarit de page. Lisez le numéro de page dans l’adresse et passez max_num_pages aux liens de pagination :
<?php
$paged = max( 1, (int) get_query_var( 'paged' ), (int) get_query_var( 'page' ) );
$news = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
) );
while ( $news->have_posts() ) {
$news->the_post();
get_template_part( 'template-parts/content', 'excerpt' );
}
echo paginate_links( array(
'total' => $news->max_num_pages,
'current' => $paged,
) );
wp_reset_postdata();
Sur une page ordinaire, /news/page/2/ remplit paged. Sur une page utilisée comme page d’accueil statique, WordPress place plutôt le numéro dans page, d’où la lecture des deux. N’utilisez pas no_found_rows ici, parce que max_num_pages dépend du comptage de lignes qu’il sauterait.
6. Filtrez par champ personnalisé seulement si c’est nécessaire. meta_query compare des valeurs enregistrées dans les métadonnées d’article, la table des champs personnalisés propres à chaque article. Cet exemple liste les événements à partir d’aujourd’hui, les plus proches en premier :
$events = new WP_Query( array(
'post_type' => 'event',
'posts_per_page' => 10,
'meta_key' => 'event_date',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => 'event_date',
'value' => wp_date( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
),
),
) );
Cela fonctionne quand event_date est enregistré au format YYYY-MM-DD, qui se trie correctement comme du texte. Les requêtes sur les métadonnées ne peuvent pas utiliser d’index sur la colonne des valeurs; elles ralentissent donc à mesure que la table grossit. Si le même filtre tourne à chaque affichage de page, une taxonomie (un regroupement semblable aux catégories) ou un résultat mis en cache est généralement plus rapide.
Validez que ça fonctionne
Vérifiez les trois endroits où une erreur de requête se manifeste. D’abord, une adresse paginée renvoie 200 et non 404 :
curl -s -o /dev/null -w '%{http_code}\n' https://staging.example.com/news/page/2/
200
Ensuite, la requête principale est toujours celle de la page. Ouvrez un article qui affiche le bloc d’articles connexes et confirmez que la section des commentaires et le titre de l’onglet du fureteur appartiennent bien à cet article, et non au dernier article connexe. Enfin, le journal de l’étape 1 montre les arguments attendus pour chaque adresse modifiée :
grep 'main query' wp-content/debug.log | tail -n 4
Pour voir de plus près combien de requêtes une page exécute et la durée de chacune, le guide de l’extension Query Monitor affiche chaque requête avec le code qui l’a lancée. Quand vous avez terminé, supprimez wp-content/mu-plugins/sh-query-probe.php et remettez WP_DEBUG à false.
Dépannage
Les commentaires, la balise de titre ou un widget de la barre latérale affichent un autre article. Une boucle secondaire s’est exécutée sans wp_reset_postdata(), ou celle d’une extension. Ajoutez la réinitialisation juste après votre bloc while. Si le problème persiste, cherchez dans le thème et les extensions des appels à the_post() sur des requêtes personnalisées sans réinitialisation après.
La page 2 du blogue ou d’une archive renvoie une erreur 404. Le gabarit exécute une requête personnalisée avec un posts_per_page différent de celui de la requête principale. WordPress décide du 404 à partir de la requête principale, avant que le code de votre gabarit s’exécute. Si la requête principale compte 3 pages et la vôtre 5, les pages 4 et 5 n’existent pas aux yeux de WordPress. Déplacez le changement dans pre_get_posts (étape 4) et supprimez la requête personnalisée.
La liste contient plus d’articles que posts_per_page. Des articles épinglés se sont ajoutés en tête. WordPress traite une requête secondaire simple comme la page d’accueil du blogue, là où les articles épinglés sont ajoutés. Réglez 'ignore_sticky_posts' => true.
Un changement dans pre_get_posts a cassé l’administration, le flux ou une autre liste. Une garde manque. Gardez is_admin() et is_main_query() comme première vérification, puis testez la condition précise (is_home(), is_post_type_archive( 'event' ), is_search()). Les flux exécutent aussi une requête principale; ajoutez ! $query->is_feed() si le changement ne doit pas s’y appliquer.
La page est devenue lente après l’ajout d’une requête. Cherchez 'posts_per_page' => -1, qui charge en mémoire tous les articles correspondants, et les meta_query sur de grosses tables. Fixez une vraie limite, ajoutez 'no_found_rows' => true quand vous ne paginez pas et 'fields' => 'ids' quand seuls les identifiants vous servent. Le guide d’optimisation de la base de données avec Query Monitor explique comment trouver la requête lente.
Quand ce n’est pas la bonne approche
Dans un thème de blocs, le bloc Boucle de requête est l’interface de l’éditeur pour WP_Query. Il peut réutiliser la requête principale du gabarit ou exécuter la sienne, avec des réglages de type de contenu, de catégorie, d’auteur et d’ordre, et il ne demande aucun PHP. Passez au code quand vous avez besoin de conditions que le bloc n’offre pas.
Si le but est de changer le fichier de gabarit qui affiche une adresse, cela se passe après la requête et c’est couvert dans le guide de la hiérarchie des gabarits. Et si vous construisez une recherche sur des milliers d’articles, ou des filtres à facettes sur de nombreux champs personnalisés, WP_Query sur MySQL aura du mal. Un index de recherche comme Elasticsearch, ou une extension qui s’appuie sur un tel index, convient mieux. La liste complète des arguments se trouve dans la référence de WP_Query, et le manuel des thèmes explique la Boucle plus en détail.
Un WordPress qui rapporte vraiment mérite un opérateur — Stack Harbor opère cette pile sous WordPress Entreprise ; réservez une consultation d’infrastructure.