Si vous avez déjà vu une page “Articles récents” qui passe de 200 ms à 2 secondes dès que vous ajoutez une boucle personnalisée, vous avez probablement fait “juste un petit get_posts()” au mauvais endroit.
Le problème / Le besoin
Vous devez afficher des listes d’articles (ou de CPT) en dehors de la boucle principale : page d’accueil, sidebar, bloc “À la une”, requête AJAX, widget, shortcode, etc. Et vous hésitez entre WP_Query et get_posts(), parce que les deux “marchent”.
Le problème vient rarement de la requête elle-même. Il vient de ce que vous faites autour : état global de la boucle, pagination, caches, champs inutiles chargés, filtres déclenchés, et parfois un thème/page builder qui rajoute sa couche.
À la fin, vous saurez choisir l’outil adapté (et le paramétrer) selon 3 critères concrets : fonctionnalité (pagination, contexte), performance (payload SQL + mémoire), maintenabilité (lisibilité, compatibilité avec les hooks).
Résumé rapide
- WP_Query : l’outil “bas niveau” complet pour requêtes WordPress (pagination, comptages, contrôle fin, boucles multiples).
- get_posts() : un wrapper pratique qui retourne directement un tableau de
WP_Post… mais avec des défauts par défaut (notammentno_found_rowset l’absence de pagination). - On va coder un mini-plugin qui expose 3 fonctions prêtes à copier/coller : une liste simple (get_posts optimisé), une liste paginée (WP_Query), et une liste “léger SQL” (WP_Query + champs réduits).
- On ajoute un shortcode (pratique avec Divi/Elementor/Avada) et une route AJAX optionnelle.
- On mesure proprement (Query Monitor + logs) et on évite les pièges classiques :
wp_reset_postdata()oublié, mauvais hook, caches non vidés.
Quand utiliser cette solution
- Vous avez besoin d’une boucle secondaire dans un template (page, single, archive custom), sans casser la boucle principale.
- Vous devez gérer la pagination (ou au moins connaître
max_num_pages). - Vous voulez optimiser une liste qui apparaît partout (header, sidebar, footer) et limiter les colonnes chargées.
- Vous intégrez la liste via shortcode ou via un module de page builder (Divi 5 / Elementor / Avada) qui consomme un shortcode.
- Vous avez des contraintes de perf (hébergement mutualisé, gros site, requêtes multiples) et vous voulez éviter de charger des objets inutiles.
Quand ne PAS utiliser cette solution
- Pour modifier la requête principale d’une archive : privilégiez
pre_get_posts(avec garde-fous), plutôt que de recréer une requête à côté. Doc : pre_get_posts. - Pour un cas purement “SQL custom” (statistiques, jointures complexes) : passez par
$wpdbavec requêtes préparées, mais acceptez que vous sortez du modèle WP. Doc : wpdb. - Pour afficher un contenu éditorial simple (ex : “3 derniers posts”) dans l’éditeur de blocs : un bloc “Query Loop” peut suffire, surtout si vous n’avez pas besoin de logique dynamique. Mais côté perf, vous n’aurez pas le même contrôle.
- Si vous récupérez un seul post par ID : utilisez
get_post()(ouget_postsmais ce serait inutilement indirect). Doc : get_post().
Prérequis / avant de commencer
- WordPress 6.9.4 (avril 2026) et PHP 8.1+.
- Un environnement de test (local ou staging). Évitez de coller des snippets sur production “pour voir”.
- Un outil de profilage : Query Monitor (plugin) pour inspecter SQL, hooks, cache. Page officielle : Query Monitor.
- Accès aux logs PHP/WordPress (au minimum
WP_DEBUG_LOGsur staging). Référence : Debug WordPress.
Côté sécurité : on va exposer un shortcode et une action AJAX optionnelle. Ça implique sanitization, escaping, et (en AJAX) un nonce.
L’approche naïve (et pourquoi l’éviter)
Voici le code que je retrouve le plus souvent dans des thèmes enfants et des snippets “copiés d’un vieux tuto”. Il fonctionne… jusqu’au jour où le site grossit.
<?php
// Exemple à éviter : mélange de get_posts(), setup_postdata() et oubli de reset.
$posts = get_posts([
'post_type' => 'post',
'posts_per_page' => 10,
]);
foreach ($posts as $post) {
setup_postdata($post);
echo '<h3>' . get_the_title() . '</h3>';
echo '<div>' . get_the_excerpt() . '</div>';
}
// Oubli fréquent : wp_reset_postdata();
?>
- État global cassé : sans
wp_reset_postdata(), vous polluez$postglobal. Résultat typique : le titre du template change, des blocs affichent le mauvais contenu, ou Elementor/Divi “perd” le contexte. - Pas de pagination :
get_posts()retourne un tableau. Vous n’avez pasmax_num_pages, nifound_posts, donc pas de pagination fiable sans refaire une requête. - Perf trompeuse :
get_posts()force par défautno_found_rowsàtrue. C’est bien pour une liste simple, mais ça surprend quand vous essayez ensuite de compter/paginer. - Escaping absent : afficher
get_the_title()sansesc_html()est une habitude dangereuse (même si WP filtre beaucoup, vous ne maîtrisez pas toujours les sources).
La bonne approche — tutoriel pas à pas
Étape 1 — Comprendre la vraie différence
WP_Query est la classe de requête. Elle gère l’exécution SQL, la boucle (have_posts()/the_post()), la pagination, et expose des propriétés utiles (found_posts, max_num_pages).
get_posts() est une fonction utilitaire qui appelle WP_Query en interne et retourne directement $query->posts (un tableau de WP_Post). Elle applique aussi des valeurs par défaut orientées “liste simple”. Référence : get_posts() et WP_Query.
Étape 2 — Décider selon 4 questions
- Avez-vous besoin de pagination ou de comptage ? → WP_Query.
- Voulez-vous juste N posts “au plus vite”, sans pages suivantes ? → get_posts() (optimisé).
- Voulez-vous éviter de charger des objets complets (mémoire) ? → WP_Query avec
fieldsouupdate_post_meta_cache/update_post_term_cacheàfalse. - Êtes-vous dans un contexte où le global
$postest fragile (builders) ? → vous pouvez évitersetup_postdata()et utiliser les IDs/objets explicitement.
Étape 3 — Installer le code au bon endroit
Le plus propre : un mini-plugin. Ça évite que le code disparaisse au changement de thème, et c’est plus facile à désactiver si vous cassez quelque chose.
- Créez
wp-content/plugins/bpcab-query-examples/bpcab-query-examples.php - Activez-le dans l’admin
Étape 4 — Ajouter un shortcode “liste de posts” (safe)
Le shortcode est un bon format d’exemple parce qu’il se teste vite, et il s’intègre bien dans Divi/Elementor/Avada. On va proposer deux modes : simple (get_posts) et paginé (WP_Query).
Étape 5 — Tester et mesurer
Avant d’optimiser “au feeling”, comparez :
- nombre de requêtes SQL
- temps SQL
- taille des objets chargés (mémoire, surtout si vous faites 5 boucles)
Code complet
Copiez-collez ce fichier tel quel dans un plugin. Il ajoute :
[bpcab_posts mode="simple"]→ get_posts optimisé[bpcab_posts mode="paged"]→ WP_Query paginé- Un mode
ids“léger” qui ne charge que des IDs, utile pour perf
<?php
/**
* Plugin Name: BPCAB Query Examples (WP_Query vs get_posts)
* Description: Exemples pratiques et optimisés de WP_Query et get_posts (WP 6.9.4+, PHP 8.1+).
* Version: 1.0.0
* Author: BPCAB
*/
if (!defined('ABSPATH')) {
exit;
}
final class BPCAB_Query_Examples {
public static function init(): void {
add_shortcode('bpcab_posts', [__CLASS__, 'shortcode_posts']);
}
/**
* Shortcode: [bpcab_posts mode="simple|paged|ids" post_type="post" per_page="5" category="slug" orderby="date" order="DESC"]
*
* Notes:
* - "simple" utilise get_posts() (rapide, pas de pagination)
* - "paged" utilise WP_Query (pagination + max_num_pages)
* - "ids" utilise WP_Query fields=ids (charge peu de mémoire)
*/
public static function shortcode_posts(array $atts = []): string {
$atts = shortcode_atts([
'mode' => 'simple',
'post_type' => 'post',
'per_page' => 5,
'category' => '', // slug
'orderby' => 'date',
'order' => 'DESC',
'class' => 'bpcab-post-list',
'paged' => '', // optionnel: forcer la page
], $atts, 'bpcab_posts');
// Sanitization stricte (shortcode = entrée utilisateur)
$mode = sanitize_key($atts['mode']);
$post_type = sanitize_key($atts['post_type']);
$per_page = max(1, min(50, absint($atts['per_page']))); // borne anti-abus
$category = sanitize_title($atts['category']);
$orderby = sanitize_key($atts['orderby']);
$order = strtoupper(sanitize_key($atts['order'])) === 'ASC' ? 'ASC' : 'DESC';
$class = sanitize_html_class($atts['class']);
// Pagination: on respecte query_var('paged') si présent, sinon 1.
// En builder, query_var peut être 0/1 selon le contexte ; on normalise.
$paged_from_query = max(1, absint(get_query_var('paged')));
$paged_from_atts = max(0, absint($atts['paged']));
$paged = $paged_from_atts > 0 ? $paged_from_atts : $paged_from_query;
// Construction d'args communs
$common_args = [
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => $per_page,
'orderby' => $orderby,
'order' => $order,
'ignore_sticky_posts' => true,
];
if ($category !== '' && $post_type === 'post') {
// Pour "post" classique, category_name est pratique (slug).
$common_args['category_name'] = $category;
}
// Rendu HTML
$out = '<div class="' . esc_attr($class) . '">';
if ($mode === 'paged') {
$out .= self::render_with_wp_query_paged($common_args, $paged);
} elseif ($mode === 'ids') {
$out .= self::render_with_wp_query_ids($common_args);
} else {
// Par défaut: simple
$out .= self::render_with_get_posts($common_args);
}
$out .= '</div>';
return $out;
}
/**
* Mode "simple": get_posts() optimisé.
* - Retourne un tableau de WP_Post
* - no_found_rows = true par défaut (OK ici)
*/
private static function render_with_get_posts(array $args): string {
// get_posts() accepte globalement les mêmes args que WP_Query.
// On force quelques optimisations "safe" pour une liste simple.
$args['no_found_rows'] = true; // pas de pagination => pas besoin de SQL_CALC_FOUND_ROWS
$args['update_post_meta_cache'] = false; // évite de précharger postmeta
$args['update_post_term_cache'] = false; // évite de précharger les terms
$posts = get_posts($args);
if (empty($posts)) {
return '<p>' . esc_html__('Aucun contenu trouvé.', 'bpcab') . '</p>';
}
$html = '<ul>';
foreach ($posts as $p) {
// Ici, on évite setup_postdata() pour ne pas toucher au global $post.
$title = get_the_title($p);
$url = get_permalink($p);
$html .= '<li>';
$html .= '<a href="' . esc_url($url) . '">' . esc_html($title) . '</a>';
$html .= '</li>';
}
$html .= '</ul>';
return $html;
}
/**
* Mode "paged": WP_Query + pagination.
* - expose max_num_pages
* - nécessite wp_reset_postdata()
*/
private static function render_with_wp_query_paged(array $args, int $paged): string {
$args['paged'] = $paged;
$args['no_found_rows'] = false; // on VEUT found_posts/max_num_pages
$args['update_post_meta_cache'] = false;
$args['update_post_term_cache'] = false;
$q = new WP_Query($args);
if (!$q->have_posts()) {
return '<p>' . esc_html__('Aucun contenu trouvé.', 'bpcab') . '</p>';
}
$html = '<ul>';
while ($q->have_posts()) {
$q->the_post();
$html .= '<li>';
$html .= '<a href="' . esc_url(get_permalink()) . '">' . esc_html(get_the_title()) . '</a>';
$html .= '</li>';
}
$html .= '</ul>';
// Pagination simple (liens précédent/suivant)
$html .= self::render_prev_next_links($paged, (int) $q->max_num_pages);
// CRITIQUE : reset de $post global
wp_reset_postdata();
return $html;
}
/**
* Mode "ids": WP_Query avec fields=ids.
* Utile quand vous voulez minimiser la mémoire et maîtriser ce que vous chargez ensuite.
*/
private static function render_with_wp_query_ids(array $args): string {
$args['fields'] = 'ids';
$args['no_found_rows'] = true;
$args['update_post_meta_cache'] = false;
$args['update_post_term_cache'] = false;
$q = new WP_Query($args);
if (empty($q->posts)) {
return '<p>' . esc_html__('Aucun contenu trouvé.', 'bpcab') . '</p>';
}
$html = '<ul>';
foreach ($q->posts as $post_id) {
$post_id = absint($post_id);
$html .= '<li>';
$html .= '<a href="' . esc_url(get_permalink($post_id)) . '">' . esc_html(get_the_title($post_id)) . '</a>';
$html .= '</li>';
}
$html .= '</ul>';
return $html;
}
/**
* Pagination précédente/suivante sans dépendre d'un contexte d'archive.
* En builder, paginate_links() peut être plus complexe à intégrer ; ici on reste minimal.
*/
private static function render_prev_next_links(int $paged, int $max_pages): string {
if ($max_pages <= 1) {
return '';
}
$html = '<div class="bpcab-pagination">';
// On reconstruit une URL "safe" basée sur l'URL actuelle.
$base_url = remove_query_arg(['bpcab_page'], home_url(add_query_arg([], $_SERVER['REQUEST_URI'] ?? '')));
$prev_page = $paged > 1 ? $paged - 1 : 0;
$next_page = $paged < $max_pages ? $paged + 1 : 0;
if ($prev_page) {
$html .= '<a class="bpcab-prev" href="' . esc_url(add_query_arg('bpcab_page', $prev_page, $base_url)) . '">';
$html .= esc_html__('← Précédent', 'bpcab');
$html .= '</a>';
}
if ($next_page) {
$html .= ' ';
$html .= '<a class="bpcab-next" href="' . esc_url(add_query_arg('bpcab_page', $next_page, $base_url)) . '">';
$html .= esc_html__('Suivant →', 'bpcab');
$html .= '</a>';
}
$html .= '</div>';
return $html;
}
}
BPCAB_Query_Examples::init();
// Petite astuce: permettre de piloter la pagination via ?bpcab_page=2
add_action('init', function (): void {
$page = isset($_GET['bpcab_page']) ? absint($_GET['bpcab_page']) : 0;
if ($page > 0) {
// On injecte "paged" dans la query var standard.
// Attention: ce n'est pas une règle globale de WP, c'est juste pour ce shortcode.
set_query_var('paged', $page);
}
});
Explication du code
Ce que fait le plugin, simplement
- Il déclare un shortcode
[bpcab_posts]qui affiche une liste de liens vers des posts. - Il propose 3 “moteurs” :
- simple :
get_posts()(rapide, sans pagination) - paged :
WP_Query(boucle complète + pagination) - ids :
WP_Queryen mode “IDs” (moins de mémoire)
- simple :
Pourquoi éviter setup_postdata() en mode simple
En mode get_posts(), on itère sur des objets WP_Post. Beaucoup de snippets font setup_postdata($post) pour réutiliser des tags de template. Le prix caché : vous touchez au global $post, et vous devez reset. Sur des pages construites avec Divi/Elementor, j’ai souvent vu des effets de bord très pénibles à diagnostiquer.
Ici, on utilise get_the_title($p) et get_permalink($p) en passant explicitement le post. C’est plus robuste.
Pourquoi WP_Query pour la pagination
La pagination nécessite found_posts et max_num_pages. get_posts() n’expose pas ces données (il ne vous donne que $query->posts). Avec WP_Query, vous récupérez $q->max_num_pages et vous pouvez construire des liens.
Les options de performance qui comptent vraiment
no_found_rows:true: WP évite le comptage total (souvent plus rapide) → parfait pour “liste simple”.false: nécessaire pour pagination fiable.
update_post_meta_cacheetupdate_post_term_cache: àfalse, WP ne précharge pas les metas/terms pour tous les posts. Sur une liste qui n’affiche que titre + lien, c’est un gain net.fields => 'ids': vous récupérez uniquement des IDs. Très utile si vous voulez ensuite charger seulement ce dont vous avez besoin (ou si vous avez un cache objet).
Sanitization et escaping
- Les attributs du shortcode sont nettoyés avec
sanitize_key(),absint(),sanitize_title(),sanitize_html_class(). - Les sorties HTML utilisent
esc_html(),esc_attr(),esc_url().
Ce duo vous évite des surprises, surtout si le shortcode est utilisable par des rôles non-admin (éditeur, auteur).
Variantes et cas d’usage
Variante 1 — Remplacer category_name par tax_query (CPT + taxonomie)
Pour un CPT (ex : portfolio) avec une taxonomie portfolio_cat, category_name ne marche pas. Utilisez tax_query.
<?php
$args = [
'post_type' => 'portfolio',
'posts_per_page' => 6,
'tax_query' => [
[
'taxonomy' => 'portfolio_cat',
'field' => 'slug',
'terms' => ['branding', 'webdesign'],
],
],
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
];
$items = get_posts($args);
?>
Référence tax_query : WP_Query taxonomy parameters.
Variante 2 — Besoin d’un “compte total” sans pagination UI
Cas réel : vous affichez “12 articles trouvés” mais vous ne paginez pas (vous chargez tout en AJAX ensuite). Dans ce cas, get_posts() est un mauvais choix, parce que vous finirez par refaire une requête juste pour compter.
Utilisez WP_Query avec posts_per_page limité, mais no_found_rows à false uniquement si vous avez vraiment besoin du total.
<?php
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 5,
'no_found_rows' => false, // on veut found_posts
]);
$total = (int) $q->found_posts;
// ... rendu partiel
wp_reset_postdata();
?>
Variante 3 — Requête “ultra légère” pour alimenter un cache
Sur des sites à fort trafic, je fais souvent un premier passage en fields=ids, puis je mets en cache la liste d’IDs (transient ou cache objet), et je rends ensuite.
<?php
$cache_key = 'bpcab_home_featured_ids_v1';
$post_ids = get_transient($cache_key);
if (!is_array($post_ids)) {
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 10,
'fields' => 'ids',
'no_found_rows' => true,
]);
$post_ids = array_map('absint', $q->posts);
set_transient($cache_key, $post_ids, 5 * MINUTE_IN_SECONDS);
}
foreach ($post_ids as $id) {
echo '<a href="' . esc_url(get_permalink($id)) . '">' . esc_html(get_the_title($id)) . '</a><br>';
}
?>
Référence transients : Transients API.
Compatibilité Divi 5 / Elementor / Avada
Divi 5
Divi consomme très bien les shortcodes via un module “Code” ou “Texte”. Le point sensible, c’est le contexte global $post : si votre snippet utilise setup_postdata() sans reset, Divi peut afficher des champs dynamiques incohérents dans les modules suivants.
- Utilisez le mode
simple(qui n’altère pas$post) quand vous n’avez pas besoin de pagination. - Si vous utilisez
paged, lewp_reset_postdata()est obligatoire (il est dans le code).
Elementor
Elementor a souvent plusieurs boucles sur une même page (widgets posts, templates, etc.). J’ai déjà vu des pages où une boucle custom fait exploser le temps SQL parce que le widget “Posts” d’Elementor déclenche aussi ses propres requêtes.
- Préférez
update_post_meta_cache=falseetupdate_post_term_cache=falsepour vos listes “titre + lien”. - Si vous devez paginer, évitez de dépendre de la pagination globale d’archive : une pagination “locale” (comme
?bpcab_page=) est plus prévisible.
Avada (Fusion Builder)
Avada supporte les shortcodes et a ses propres éléments de blog. Les conflits typiques viennent du cache (Avada + plugin de cache) et des pages mises en cache avec un paramètre ?bpcab_page=.
- Si votre page est cachée côté serveur, configurez le cache pour varier selon la query string (ou désactivez le cache sur cette page).
- Testez en navigation privée et après purge des caches (plugin + CDN).
Vérifications après mise en place
- Ajoutez le shortcode dans une page :
[bpcab_posts], puis[bpcab_posts mode="paged" per_page="5"]. - Vérifiez que le reste de la page (titre, contenu, modules builder) n’a pas changé après la liste. Si ça change, c’est généralement un
wp_reset_postdata()manquant dans un autre snippet. - Avec Query Monitor :
- comparez le nombre de requêtes SQL entre
mode="simple"etmode="paged" - regardez si vous avez des requêtes
postmetainutiles (signe que vous préchargez trop)
- comparez le nombre de requêtes SQL entre
Tableau de diagnostic rapide
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Le titre/la vignette du reste de la page change après la liste | setup_postdata() sans wp_reset_postdata() |
Ajoutez un var_dump($post->ID) avant/après (staging) ou inspectez les hooks avec Query Monitor |
Ajoutez wp_reset_postdata() après la boucle, ou évitez setup_postdata() en passant l’ID aux fonctions |
| Pagination qui ne change rien | paged non pris en compte / page cachée |
Testez ?bpcab_page=2 en navigation privée, désactivez cache |
Utilisez WP_Query avec paged, purgez les caches, configurez le cache avec variation par query string |
| Temps SQL élevé sur une simple liste | Préchargement metas/terms ou trop de boucles | Query Monitor > requêtes postmeta/term_relationships |
update_post_meta_cache=false, update_post_term_cache=false, réduire le nombre de requêtes |
| Erreur 500 après collage du code | Erreur de syntaxe (point-virgule, parenthèse), mauvais emplacement | Consultez wp-content/debug.log ou logs serveur |
Revenir en arrière via FTP, activer le plugin “Health Check” si besoin, corriger la syntaxe |
Si ça ne marche pas
- Vérifiez l’emplacement : le code doit être dans un plugin (recommandé) ou dans
functions.phpd’un thème enfant. Coller ça dans l’éditeur de page ne peut pas marcher. - Regardez les logs : une parenthèse manquante suffit à faire une 500. Référence debug : Debug WordPress.
- Désactivez temporairement le cache (plugin cache + CDN). J’ai souvent vu une pagination “cassée” qui était juste une page HTML servie depuis un cache agressif.
- Testez sans builder : mettez le shortcode dans une page simple (thème par défaut si possible). Si ça marche là, vous avez un conflit de contexte (global
$post) ou de cache builder. - Vérifiez la version PHP : le code utilise des types de retour (
: void) etfinal class. Si vous êtes en PHP < 8.1, vous allez souffrir ailleurs aussi. Référence : Versions supportées de PHP.
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
| “Call to undefined function …” | Code exécuté trop tôt (mauvais hook) ou fichier non chargé | Mettre le code dans un plugin activé, ou dans functions.php. Éviter d’appeler des fonctions WP avant init si vous faites des choses avancées. |
| La boucle principale est “décalée” après votre liste | wp_reset_postdata() oublié après WP_Query |
Ajoutez wp_reset_postdata() juste après la boucle. |
| Pagination toujours à 1 | Vous utilisez get_posts() en pensant que paged marche comme une archive |
Utilisez WP_Query et lisez max_num_pages. get_posts() n’est pas prévu pour paginer proprement. |
| Requêtes SQL “FOUND_ROWS()” coûteuses | no_found_rows=false partout par copier-coller |
Mettez no_found_rows=true dès que vous n’avez pas besoin de pagination/total. |
| Le snippet marche puis “disparaît” après mise à jour du thème | Code collé dans le thème parent | Utilisez un thème enfant ou, mieux, un mini-plugin. |
| Résultats incohérents avec un plugin multilingue | Filtres de requête (langue) et suppression via suppress_filters |
Évitez suppress_filters=true sauf cas très spécifique. Testez avec WPML/Polylang si vous en avez. |
| “Allowed memory size exhausted” sur une grosse liste | Vous chargez 500 posts complets + metas + terms | Réduisez posts_per_page, utilisez fields=>ids, désactivez caches meta/term si inutiles, paginez. |
| Les styles ne s’appliquent pas dans le builder | CSS non chargé (mauvais enqueue) ou cache | Enqueue via wp_enqueue_scripts et purgez les caches. (Ici on n’ajoute pas de CSS, mais c’est un classique.) |
Conseils sécurité, performance et maintenance
- Ne faites pas confiance aux attributs de shortcode : même si seuls les admins éditent, un import de contenu ou un rôle mal configuré suffit. Sanitization + escaping, systématique.
- Évitez les boucles imbriquées avec
the_post()partout. Si vous devez afficher 3 listes sur une page, le modeids+ cache est souvent plus stable. - Attention à
orderby: certainsorderby(meta_value, rand) coûtent cher. Pour des pages très visitées, évitezrandsans cache. - Cache page vs pagination locale : si vous paginez via query string (
?bpcab_page=2), votre cache doit varier selon la query string, sinon vous servirez toujours la page 1. - Compatibilité future : rester sur
WP_Query/get_postsvous protège des changements internes. Évitez les requêtes SQL maison pour des listes standard.
Référence arguments WP_Query (la liste complète, à garder sous la main) : WP_Query parameters.
Ressources
- WP_Query (référence)
- get_posts() (référence)
- wp_reset_postdata() (référence)
- Hook pre_get_posts (référence)
- Query Monitor (plugin)
- Transients API
- wordpress-develop sur GitHub (source du core)
- WordPress Core Trac (tickets et discussions)
- PHP : déclarations de types
FAQ
get_posts() est-il “plus rapide” que WP_Query ?
Pas intrinsèquement. get_posts() appelle WP_Query sous le capot. La différence vient surtout des valeurs par défaut (notamment no_found_rows) et de ce que vous faites ensuite (setup/reset, metas/terms, nombre de boucles).
Pourquoi get_posts() ne me donne pas max_num_pages ?
Parce qu’il retourne uniquement un tableau de posts, pas l’objet requête. Si vous avez besoin de max_num_pages, utilisez WP_Query.
Dois-je toujours mettre no_found_rows=true pour optimiser ?
Non. Si vous paginez ou si vous devez afficher un total fiable, vous avez besoin de no_found_rows=false. Sinon, mettez true.
update_post_meta_cache=false peut-il casser mon rendu ?
Oui si votre template appelle ensuite get_post_meta() pour chaque post. Dans ce cas, vous déplacerez le coût (une requête par post). Si vous affichez seulement titre/lien/date, c’est généralement un gain net.
fields=>’ids’ est-il toujours une bonne idée ?
Très bon pour la mémoire et le transport de données, mais attention : si vous finissez par appeler 10 fonctions qui chacune charge le post complet, vous pouvez perdre le bénéfice. Utilisez-le quand vous contrôlez précisément ce que vous chargez ensuite.
Je peux utiliser get_posts() dans un widget/shortcode sans wp_reset_postdata() ?
Oui si vous n’appelez pas setup_postdata(). Si vous appelez setup_postdata(), vous devez reset.
Pourquoi ma pagination ne marche pas dans Elementor/Divi ?
Souvent à cause du cache (page mise en cache identique pour toutes les query strings) ou parce que get_query_var('paged') ne reflète pas ce que vous pensez sur une page statique. Une pagination “locale” via query string (comme ?bpcab_page=) est plus prévisible.
Est-ce que je dois utiliser query_posts() ?
Non, sauf cas très particulier. query_posts() remplace la requête principale et casse facilement la pagination et les globals. Préférez pre_get_posts pour la requête principale, ou WP_Query pour une requête secondaire.
Comment savoir si je fais trop de requêtes ?
Installez Query Monitor et regardez : nombre de requêtes, requêtes lentes, et surtout les requêtes répétées sur postmeta. C’est le signal le plus fréquent d’une boucle “trop gourmande”.
Est-ce que ces exemples sont compatibles WordPress 6.9.4+ ?
Oui. Ils reposent sur des API stables (WP_Query, get_posts, escaping, shortcodes) et sur une syntaxe PHP compatible 8.1+. Si vous utilisez un vieux tutoriel avec des patterns datés (ex : query_posts() partout), migrez vers ces approches.