Si vous avez déjà tenté de “récupérer un contenu WordPress depuis WordPress” (un article, des champs, un menu, un bloc, une image) et que vous vous êtes retrouvé avec une boucle infinie, une requête lente, ou du HTML imprévisible, vous avez touché un classique : utiliser WordPress efficacement dans WordPress, ce n’est pas “faire une requête SQL et afficher le résultat”.
WordPress 6.9.4 (avril 2026) vous donne un ensemble d’API stables pour interroger, mettre en cache, rendre et sécuriser des contenus. Le vrai gain vient quand vous arrêtez de “tirer” de la donnée au hasard, et que vous laissez le cœur faire ce qu’il sait faire : requêtes WP_Query, caches d’objets, rendu de blocs, contextes de template, et échappement.
Le problème / Le besoin
Besoin concret : afficher (ou réutiliser) des contenus WordPress à plusieurs endroits du site, sans dupliquer, sans casser la perf, et sans créer une usine à gaz dans votre thème.
Exemples que je vois tout le temps en support :
- Un “encart auteur” identique sur 200 articles, mais avec des variations selon la catégorie.
- Un bloc “articles liés” qui doit fonctionner dans le contenu, dans un widget, et dans un module de page builder.
- Un “mini annuaire” (CPT) affiché sur une page, avec filtres simples, sans plugin lourd.
- Réutiliser du contenu d’une page “Mentions légales” dans le footer, sans copier-coller.
À la fin, vous saurez :
- Créer un petit plugin “contenu réutilisable” propre (shortcode + endpoint REST optionnel).
- Interroger des contenus avec WP_Query sans pièges classiques.
- Rendre du contenu correctement (blocs, shortcodes, images) sans double traitement.
- Mettre en cache intelligemment (transients + invalidation) pour éviter les requêtes répétées.
- Brancher le tout sur Divi 5, Elementor et Avada sans hacks.
Résumé rapide
- On crée un plugin minimal (PHP 8.1+) qui expose un shortcode
[wp_reuse]pour afficher un post/page/CPT par ID ou slug. - On utilise
WP_Query(ouget_post()quand c’est suffisant), en évitant les requêtes inutiles. - On rend le contenu via
the_contentde façon contrôlée, avec échappement et options (extrait, titre, image). - On ajoute un cache par transient, et on l’invalide quand le contenu source change.
- On fournit une variante REST (optionnelle) pour des intégrations JS/page builders plus propres.
Quand utiliser cette solution
- Vous voulez réutiliser un même contenu à plusieurs endroits (footer, sidebar, pages, templates, modules).
- Vous avez besoin d’un “composant” WordPress simple, sans installer un plugin de blocs complet.
- Vous voulez un rendu cohérent avec le thème (filtres
the_content, images, embeds). - Vous devez garder la main sur la performance (cache + requêtes minimales).
- Vous travaillez avec des page builders et vous voulez une brique stable (shortcode ou REST).
Quand ne PAS utiliser cette solution
- Vous avez juste besoin d’un lien ou d’un extrait statique : un bloc “HTML personnalisé” suffit.
- Vous devez gérer des variations complexes par rôle/utilisateur en temps réel : préférez un rendu côté template avec logique métier (et éventuellement un cache par utilisateur, plus délicat).
- Vous voulez une expérience d’édition 100% Gutenberg avec UI avancée : un bloc custom (block.json + React) est plus adapté que des shortcodes.
- Vous avez un besoin d’agrégation massive (type “page magazine” avec 30 requêtes) : il faut une stratégie de requêtes groupées, voire un système de pré-calcul (cron) et cache persistant.
Prérequis / avant de commencer
Ce qui évite 80% des catastrophes :
- Un environnement de staging (ou local) et une sauvegarde avant de tester. Ne testez pas ce genre de snippet sur production.
- WordPress 6.9.4 et PHP 8.1+ (idéalement 8.2/8.3 si votre hébergeur suit).
- Accès FTP/SSH pour récupérer le site en cas d’erreur fatale.
- Un plugin de logs (ou au minimum
WP_DEBUG_LOG) pour lire les erreurs.
Outils utiles :
- Query Monitor (pour voir les requêtes et les hooks) : wordpress.org/plugins/query-monitor
- La doc officielle WP_Query : developer.wordpress.org/reference/classes/wp_query
- REST API Handbook : developer.wordpress.org/rest-api
- Transients API : developer.wordpress.org/apis/handbook/transients
- PHP filter_var / sanitization : php.net/manual/en/function.filter-var.php
Précaution sécurité : dès que vous acceptez des paramètres (shortcode, REST, query string), vous devez valider et sanitizer les entrées, et échapper les sorties. Un shortcode mal verrouillé devient vite une fuite de contenu (ex : afficher un post privé par ID).
L'approche naïve (et pourquoi l'éviter)
J’ai souvent vu ce “snippet magique” circuler : une requête SQL directe, puis un echo du contenu. Ça marche… jusqu’au jour où ça ne marche plus (multisite, cache, révisions, filtres, blocs, sécurité).
<?php
// ❌ Exemple à éviter : SQL direct + pas de permissions + pas de filtres WordPress.
global $wpdb;
$id = $_GET['id']; // ❌ Non sanitizé
$row = $wpdb->get_row("SELECT post_content FROM {$wpdb->posts} WHERE ID = $id"); // ❌ Injection SQL
echo $row->post_content; // ❌ Pas de the_content, pas d'escapement, pas de gestion des blocs
?>
Pourquoi c’est un problème :
- Sécurité : injection SQL, fuite de contenu privé, contournement des capacités.
- Rendu : le contenu des blocs n’est pas rendu correctement si vous ne passez pas par le pipeline WordPress (
the_content+ rendu de blocs). - Compatibilité : vous contournez les caches et les filtres des plugins (SEO, shortcodes, embeds).
- Maintenance : vous réinventez WP_Query en moins bien.
La bonne approche — tutoriel pas à pas
Étape 1 — Créez un mini-plugin (plutôt qu’un snippet dans functions.php)
Dans mon expérience, la moitié des sites cassés viennent d’un code collé dans le mauvais functions.php (thème parent mis à jour, thème enfant absent, ou plugin de snippets qui se désactive). Un plugin dédié est plus stable.
Créez un dossier : wp-content/plugins/wp-reuse-content/ puis un fichier : wp-reuse-content.php.
Étape 2 — Définissez un shortcode “réutilisable”
Objectif : [wp_reuse id="123"] ou [wp_reuse slug="mentions-legales"], avec options : afficher le titre, l’extrait, l’image mise en avant, et choisir si on applique the_content.
Points clés :
- Validez les attributs (id entier positif, slug propre).
- Respectez la visibilité : ne pas afficher un post privé si l’utilisateur n’a pas le droit.
- Évitez les requêtes lourdes : si vous avez un ID,
get_post()suffit souvent.
Étape 3 — Ajoutez un cache par transient
Sans cache, un shortcode dans un footer peut déclencher une requête sur chaque page. Et si vous l’utilisez 3 fois, c’est 3 requêtes (ou plus si vous faites mal les choses). Un transient HTML par “combinaison d’options” est simple et efficace.
On invalide le cache quand le post source est modifié (hook save_post), quand il est supprimé, ou quand son statut change.
Étape 4 — (Optionnel) Exposez un endpoint REST
Pour Elementor/Divi/Avada, le shortcode suffit souvent. Mais si vous faites du rendu dynamique côté JS (ou si vous avez un front headless partiel), un endpoint REST propre évite de parser du HTML au mauvais endroit.
On sécurise avec :
permission_callbackstrict (au minimum, ne pas exposer les contenus non publics).- Validation des paramètres via
argsderegister_rest_route.
Code complet
Copiez-collez ce fichier tel quel dans wp-content/plugins/wp-reuse-content/wp-reuse-content.php, activez-le, puis testez avec [wp_reuse id="123"].
<?php
/**
* Plugin Name: WP Reuse Content (Shortcode + Cache + REST)
* Description: Réutiliser efficacement du contenu WordPress (pages/articles/CPT) via shortcode, avec cache et endpoint REST optionnel.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
* Author: Votre Nom
* License: GPLv2 or later
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class WP_Reusables_Content {
private const TRANSIENT_PREFIX = 'wprc_html_';
private const TRANSIENT_TTL = 6 * HOUR_IN_SECONDS;
public static function init(): void {
add_shortcode( 'wp_reuse', [ __CLASS__, 'shortcode' ] );
// Invalidation cache
add_action( 'save_post', [ __CLASS__, 'purge_cache_for_post' ], 10, 2 );
add_action( 'deleted_post', [ __CLASS__, 'purge_cache_for_post_id' ], 10, 1 );
add_action( 'transition_post_status', [ __CLASS__, 'purge_cache_on_status_change' ], 10, 3 );
// REST (optionnel mais utile)
add_action( 'rest_api_init', [ __CLASS__, 'register_rest_routes' ] );
}
/**
* Shortcode: [wp_reuse id="123" slug="mentions-legales" type="page" title="1" featured="0" excerpt="0" apply_content_filters="1" class="ma-classe"]
*/
public static function shortcode( array $atts = [], string $content = '', string $shortcode_tag = '' ): string {
$atts = shortcode_atts(
[
'id' => '',
'slug' => '',
'type' => 'any',
'title' => '0',
'featured' => '0',
'excerpt' => '0',
'apply_content_filters' => '1',
'class' => '',
],
$atts,
'wp_reuse'
);
$post_id = self::sanitize_positive_int( $atts['id'] );
$slug = self::sanitize_slug( (string) $atts['slug'] );
$type = self::sanitize_post_type( (string) $atts['type'] );
$show_title = self::to_bool( $atts['title'] );
$show_featured = self::to_bool( $atts['featured'] );
$show_excerpt = self::to_bool( $atts['excerpt'] );
$apply_filters = self::to_bool( $atts['apply_content_filters'] );
$extra_class = sanitize_html_class( (string) $atts['class'] );
if ( ! $post_id && ! $slug ) {
return '';
}
// Cache key dépend de la cible + options de rendu.
$cache_key = self::build_cache_key(
[
'id' => $post_id,
'slug' => $slug,
'type' => $type,
'title' => $show_title ? 1 : 0,
'featured' => $show_featured ? 1 : 0,
'excerpt' => $show_excerpt ? 1 : 0,
'apply_filters' => $apply_filters ? 1 : 0,
'class' => $extra_class,
// Variation par langue/locale si vous utilisez des plugins multilingues.
'locale' => determine_locale(),
]
);
$cached = get_transient( $cache_key );
if ( is_string( $cached ) && $cached !== '' ) {
return $cached;
}
$post = self::resolve_post( $post_id, $slug, $type );
if ( ! $post ) {
return '';
}
// Respect de la visibilité (privé, brouillon, etc.)
if ( ! self::can_view_post( $post ) ) {
return '';
}
$html = self::render_post_html(
$post,
[
'show_title' => $show_title,
'show_featured' => $show_featured,
'show_excerpt' => $show_excerpt,
'apply_filters' => $apply_filters,
'extra_class' => $extra_class,
]
);
// Cache HTML (attention: si rendu dépend d'un utilisateur connecté, ne pas cacher globalement).
set_transient( $cache_key, $html, self::TRANSIENT_TTL );
return $html;
}
private static function resolve_post( int $post_id, string $slug, string $type ): ?WP_Post {
if ( $post_id > 0 ) {
$post = get_post( $post_id );
if ( $post instanceof WP_Post ) {
// Si un type est demandé, on vérifie.
if ( $type !== 'any' && $post->post_type !== $type ) {
return null;
}
return $post;
}
return null;
}
// Résolution par slug : WP_Query (car get_page_by_path est limité et peut être ambigu sur CPT).
$args = [
'name' => $slug,
'post_type' => ( $type === 'any' ) ? get_post_types( [ 'public' => true ], 'names' ) : $type,
'post_status' => [ 'publish', 'private' ],
'posts_per_page' => 1,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
];
$q = new WP_Query( $args );
if ( ! $q->have_posts() ) {
return null;
}
$post = $q->posts[0] ?? null;
return ( $post instanceof WP_Post ) ? $post : null;
}
private static function can_view_post( WP_Post $post ): bool {
$status = get_post_status( $post );
if ( $status === 'publish' ) {
return true;
}
// Post privé : uniquement si l'utilisateur a la capacité de le lire.
if ( $status === 'private' ) {
return current_user_can( 'read_post', $post->ID );
}
// Brouillon, pending, etc. : on ne rend pas via shortcode public.
return false;
}
private static function render_post_html( WP_Post $post, array $opts ): string {
$show_title = (bool) ( $opts['show_title'] ?? false );
$show_featured = (bool) ( $opts['show_featured'] ?? false );
$show_excerpt = (bool) ( $opts['show_excerpt'] ?? false );
$apply_filters = (bool) ( $opts['apply_filters'] ?? true );
$extra_class = (string) ( $opts['extra_class'] ?? '' );
$classes = [ 'wprc-reuse' ];
if ( $extra_class ) {
$classes[] = $extra_class;
}
// On construit du HTML maîtrisé. On échappe attributs et titres.
$out = '<div class="' . esc_attr( implode( ' ', $classes ) ) . '" data-source-id="' . esc_attr( (string) $post->ID ) . '">';
if ( $show_title ) {
$out .= '<h3 class="wprc-title">' . esc_html( get_the_title( $post ) ) . '</h3>';
}
if ( $show_featured && has_post_thumbnail( $post ) ) {
// Taille "large" généralement OK; adaptez selon votre thème.
$out .= '<div class="wprc-featured">' . get_the_post_thumbnail( $post, 'large', [ 'loading' => 'lazy' ] ) . '</div>';
}
if ( $show_excerpt ) {
$excerpt = get_the_excerpt( $post );
if ( $excerpt !== '' ) {
$out .= '<div class="wprc-excerpt">' . wp_kses_post( wpautop( $excerpt ) ) . '</div>';
}
}
$content = $post->post_content;
// Rendu du contenu : apply_filters('the_content') déclenche rendu blocs, embeds, shortcodes, etc.
// Attention: ne faites pas ça deux fois, sinon vous aurez des <p> en double et des shortcodes dupliqués.
if ( $apply_filters ) {
$content = apply_filters( 'the_content', $content );
} else {
// Rendu minimal sécurisé.
$content = wp_kses_post( wpautop( $content ) );
}
$out .= '<div class="wprc-content">' . $content . '</div>';
$out .= '</div>';
return $out;
}
private static function build_cache_key( array $data ): string {
// On hash pour garder une clé courte et stable.
$payload = wp_json_encode( $data );
return self::TRANSIENT_PREFIX . md5( (string) $payload );
}
public static function purge_cache_for_post( int $post_id, WP_Post $post ): void {
self::purge_cache_for_post_id( $post_id );
}
public static function purge_cache_on_status_change( string $new_status, string $old_status, WP_Post $post ): void {
if ( $new_status === $old_status ) {
return;
}
self::purge_cache_for_post_id( (int) $post->ID );
}
public static function purge_cache_for_post_id( int $post_id ): void {
// Stratégie simple : on supprime tous les transients du plugin en base.
// Ce n'est pas parfait, mais fiable sans index.
// Sur gros sites, préférez un cache persistant (Redis) + group versioning.
global $wpdb;
$like = esc_sql( '_transient_' . self::TRANSIENT_PREFIX . '%' );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '{$like}'" );
$like_timeout = esc_sql( '_transient_timeout_' . self::TRANSIENT_PREFIX . '%' );
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '{$like_timeout}'" );
}
public static function register_rest_routes(): void {
register_rest_route(
'wprc/v1',
'/reuse',
[
'methods' => 'GET',
'callback' => [ __CLASS__, 'rest_get_reuse' ],
'permission_callback' => [ __CLASS__, 'rest_permissions' ],
'args' => [
'id' => [
'type' => 'integer',
'required' => false,
'sanitize_callback' => 'absint',
'validate_callback' => static fn( $v ) => (int) $v >= 0,
],
'slug' => [
'type' => 'string',
'required' => false,
'sanitize_callback' => 'sanitize_title',
],
'type' => [
'type' => 'string',
'required' => false,
'sanitize_callback' => 'sanitize_key',
],
],
]
);
}
public static function rest_permissions( WP_REST_Request $request ): bool {
// Endpoint public, mais on ne doit jamais exposer des posts non publics.
// La vérification fine est faite dans can_view_post().
return true;
}
public static function rest_get_reuse( WP_REST_Request $request ): WP_REST_Response {
$post_id = absint( $request->get_param( 'id' ) );
$slug = sanitize_title( (string) $request->get_param( 'slug' ) );
$type = sanitize_key( (string) $request->get_param( 'type' ) );
$type = $type ? self::sanitize_post_type( $type ) : 'any';
$post = self::resolve_post( $post_id, $slug, $type );
if ( ! $post ) {
return new WP_REST_Response( [ 'found' => false ], 404 );
}
if ( ! self::can_view_post( $post ) ) {
// Ne pas indiquer si le post existe réellement.
return new WP_REST_Response( [ 'found' => false ], 404 );
}
$html = self::render_post_html(
$post,
[
'show_title' => true,
'show_featured' => false,
'show_excerpt' => false,
'apply_filters' => true,
'extra_class' => '',
]
);
return new WP_REST_Response(
[
'found' => true,
'id' => $post->ID,
'title' => get_the_title( $post ),
'html' => $html,
'link' => get_permalink( $post ),
'updated' => get_post_modified_time( 'c', true, $post ),
],
200
);
}
private static function sanitize_positive_int( mixed $value ): int {
$v = is_scalar( $value ) ? (int) $value : 0;
return max( 0, $v );
}
private static function sanitize_slug( string $value ): string {
$value = trim( $value );
if ( $value === '' ) {
return '';
}
return sanitize_title( $value );
}
private static function sanitize_post_type( string $type ): string {
$type = sanitize_key( $type );
if ( $type === '' || $type === 'any' ) {
return 'any';
}
// Ne validez que des post types existants et publics (évite d'exposer des CPT internes).
$public_types = get_post_types( [ 'public' => true ], 'names' );
return in_array( $type, $public_types, true ) ? $type : 'any';
}
private static function to_bool( mixed $value ): bool {
// Accepte "1", "true", 1, true, etc.
if ( is_bool( $value ) ) {
return $value;
}
$v = strtolower( trim( (string) $value ) );
return in_array( $v, [ '1', 'true', 'yes', 'on' ], true );
}
}
WP_Reusables_Content::init();
Explication du code
Pourquoi un shortcode + cache est souvent le meilleur compromis
Un shortcode est “universel” : il marche dans l’éditeur de blocs, dans un widget texte, dans un module Divi/Elementor/Avada, et même dans certains champs ACF. C’est rarement la solution la plus élégante côté UX, mais c’est la plus robuste pour réutiliser du contenu sans dépendre d’un builder.
Le cache par transient évite le piège classique : un footer qui appelle 1 page réutilisée, sur 20 000 pages vues/jour, ça devient vite un goulet d’étranglement si vous déclenchez WP_Query à chaque fois.
Résolution du contenu : ID d’abord, slug ensuite
Quand vous avez un ID, get_post() est direct et rapide. Quand vous n’avez qu’un slug, on passe par WP_Query parce que la résolution “par chemin” peut devenir ambiguë sur des CPT et hiérarchies.
Optimisations WP_Query utilisées :
no_found_rows: évite le COUNT(*) pour la pagination.update_post_meta_cacheetupdate_post_term_cacheàfalse: on ne charge pas des caches inutiles si on n’en a pas besoin.posts_per_pageà 1 : on s’arrête dès qu’on a trouvé.
Rendu : appliquer (ou non) the_content
Le point qui fait la différence en 2026 : la majorité des contenus sont des blocs. Si vous affichez $post->post_content brut, vous verrez du HTML partiel, des commentaires de blocs, et des shortcodes non exécutés.
apply_filters('the_content', ...) déclenche :
- Le rendu des blocs (block renderer).
- Les shortcodes.
- Les embeds (oEmbed), selon configuration.
- Les filtres des plugins (SEO, typographie, etc.).
Piège que je vois souvent : appliquer the_content deux fois (par exemple dans un template qui appelle déjà the_content()). Résultat : double <p>, shortcodes dupliqués, ou rendu cassé. Ici, on applique une seule fois, dans le plugin.
Sécurité : visibilité et échappement
Deux couches :
- Visibilité :
can_view_post()refuse tout ce qui n’est pas publié (sauf privé avec capacitéread_post). - Échappement : attributs via
esc_attr(), titres viaesc_html(), contenu viawp_kses_post()si on n’applique pasthe_content.
Invalidation du cache
La partie “sale” du code, volontairement simple : on supprime les transients du plugin en base via un DELETE SQL. Sur un petit/moyen site, c’est acceptable.
Sur un gros site, je préfère une stratégie “versioning” (un numéro de version stocké en option, inclus dans la clé de cache), ou un cache persistant (Redis/Memcached) avec groupes. Mais pour un blog intermédiaire, ce compromis est souvent le plus pragmatique.
Variantes et cas d'usage
Variante 1 — Réutiliser seulement un extrait (rapide et léger)
Si vous voulez un encart “résumé” sans déclencher tout le pipeline de rendu, désactivez apply_content_filters et activez excerpt.
<!-- Dans un contenu ou un module builder -->
[wp_reuse id="123" excerpt="1" apply_content_filters="0" class="encart-resume"]
Avantage : moins de risques de double rendu, et plus léger. Inconvénient : vous perdez les blocs/embeds du contenu complet.
Variante 2 — Afficher un CPT spécifique par slug
Si vous avez un CPT public partner et que vous voulez réutiliser la fiche “acme” :
[wp_reuse slug="acme" type="partner" title="1" featured="1"]
Le code valide le post type contre les types publics existants, ce qui évite d’exposer un post type interne.
Variante 3 — Consommer via REST côté JavaScript
Cas typique : vous avez un widget front qui charge un contenu à la demande (onglets, modal, accordéon). Vous pouvez appeler :
curl "https://example.com/wp-json/wprc/v1/reuse?slug=mentions-legales&type=page"
Et injecter html dans votre composant. Si vous faites ça, surveillez les effets de bord JS (scripts inline) : certains contenus supposent d’être rendus au chargement initial.
Compatibilité Divi 5 / Elementor / Avada
Divi 5
Divi accepte les shortcodes dans plusieurs modules (Texte, Code, parfois dans des champs dynamiques selon votre configuration). Le plus fiable : un module Texte avec le shortcode.
- Ajoutez un module Texte.
- Collez
[wp_reuse id="123" title="1"]. - Si vous ne voyez rien, désactivez temporairement la minification/optimisation Divi et videz le cache statique.
Piège Divi fréquent : si vous utilisez un module “Code” avec des balises HTML autour, Divi peut filtrer/encoder le shortcode. Dans ce cas, utilisez le module Texte, ou activez l’option d’exécution des shortcodes si votre setup l’exige.
Elementor
Elementor gère bien les shortcodes via le widget “Shortcode”. C’est le chemin le plus stable.
- Ajoutez le widget “Shortcode”.
- Collez
[wp_reuse slug="mentions-legales" type="page"]. - Si le rendu diffère entre l’éditeur et le front, c’est souvent un problème de cache (Elementor + plugin de cache). Videz les deux.
Option plus avancée : si vous avez un widget HTML/JS custom, consommez l’endpoint REST pour charger le contenu à la demande.
Avada (Fusion Builder)
Avada propose un élément “Shortcode” et accepte aussi les shortcodes dans certains éléments de texte. Je recommande l’élément dédié “Shortcode” pour éviter les encodages.
- Ajoutez l’élément Shortcode.
- Collez
[wp_reuse id="123" featured="1"]. - Si Avada met en cache agressivement, purgez “Avada Cache” + cache serveur/CDN.
Vérifications après mise en place
- Testez le shortcode dans une page simple, sans builder, pour valider la base.
- Testez avec un post publié, puis un post privé (connecté/déconnecté) pour vérifier la sécurité.
- Ouvrez Query Monitor : vérifiez que le shortcode ne déclenche pas 5 requêtes inutiles.
- Modifiez le contenu source et vérifiez que la version réutilisée se met à jour (sinon, invalidation cache à revoir).
- Si vous utilisez un plugin de cache de page, videz-le après activation du plugin et après modifications.
Diagnostic rapide (utile quand “ça marche sur une page mais pas ailleurs”) :
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Le shortcode affiche une chaîne vide | ID/slug incorrect, ou post non publié | Ouvrez le post dans l’admin, vérifiez l’ID et le statut | Corrigez l’ID/slug, publiez le contenu |
| Le rendu est “brut” (commentaires de blocs visibles) | apply_content_filters="0" ou contenu non filtré |
Testez avec apply_content_filters="1" |
Activez les filtres, évitez de filtrer deux fois |
| Le contenu ne se met pas à jour | Cache (transient, cache page, CDN) | Désactivez temporairement le cache de page | Purger caches, réduire TTL, améliorer invalidation |
| Erreur 500 après collage | Syntaxe PHP (point-virgule manquant), mauvais fichier | Consultez wp-content/debug.log |
Corrigez la syntaxe, utilisez un plugin plutôt que functions.php |
| Le contenu privé est visible | Contrôle de capacité absent (dans un autre snippet) | Test déconnecté + post privé | Bloquez via current_user_can('read_post') |
Si ça ne marche pas
- Vérifiez l’emplacement du code : le fichier doit être dans
wp-content/plugins/wp-reuse-content/wp-reuse-content.phpet le plugin activé. - Regardez les erreurs PHP : activez temporairement
WP_DEBUGetWP_DEBUG_LOG, puis ouvrezwp-content/debug.log. Une parenthèse manquante arrive vite. - Testez un ID connu : créez une page “Test Reuse”, publiez, puis utilisez son ID.
- Désactivez les optimisations : cache de page, minification, optimisation CSS/JS (souvent la cause quand un builder ne reflète pas le front).
- Conflit de shortcode : si un autre plugin utilise
[wp_reuse](rare mais possible), changez le tag dansadd_shortcode. - Permaliens : si vous utilisez le REST et que ça renvoie 404, régénérez les permaliens (Réglages → Permaliens → Enregistrer).
- Hook trop tôt : si vous avez déplacé du code, assurez-vous que
add_shortcodeest appelé après le chargement des plugins (ici, c’est OK car dans le fichier plugin).
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
| Coller le code dans le mauvais fichier | Ajout dans functions.php du thème parent, écrasé à la mise à jour |
Créer un plugin dédié (comme ici) ou un thème enfant |
Parse error: syntax error, unexpected ... |
Point-virgule ou accolade manquante | Relire la zone signalée dans debug.log, utiliser un IDE avec formatage |
| Contenu dupliqué / paragraphes doublés | apply_filters('the_content') appliqué deux fois |
N’appliquez the_content qu’à un seul endroit (plugin OU template) |
| Le shortcode marche dans l’éditeur mais pas sur le front | Cache de page/CDN, ou minification qui sert une vieille version | Purger cache plugin + serveur + CDN, tester en navigation privée |
| Le shortcode affiche un post privé à tous | Snippet trouvé sur un vieux tutoriel sans contrôle d’accès | Vérifier post_status + current_user_can('read_post', $id) |
| Requêtes lentes | WP_Query non optimisé, meta/term cache chargé pour rien | Utiliser no_found_rows, désactiver caches inutiles, limiter à 1 résultat |
Call to undefined function |
Code d’un tutoriel ancien, ou exécution avant chargement du core | Vérifier la doc WP 6.9.4, exécuter via hooks standards, éviter les fonctions obsolètes |
| JS/CSS non appliqué au contenu réutilisé | Le contenu dépend d’assets enqueued ailleurs | Enqueue global ou conditionnel, ou préférez un bloc/plugin qui gère ses assets |
Conseils sécurité, performance et maintenance
- Ne cachez pas du contenu dépendant de l’utilisateur : si vous affichez des infos “Mon compte”, ne mettez pas ça dans un transient global. Sinon vous servez le HTML d’un utilisateur à un autre.
- Surveillez les filtres the_content : certains plugins ajoutent des scripts/iframes. Si vous réutilisez un contenu dans un endroit inattendu (ex. footer), vous pouvez casser le layout.
- Cache persistant : si vous avez Redis/Memcached, les transients deviennent plus efficaces. Sinon, ils sont stockés en base (table
wp_options), ce qui reste OK à petite échelle. - Invalidation : la purge “globale” est simple mais brute. Si votre site grossit, passez à une stratégie de version (une option
wprc_cache_versionincrémentée, incluse dans la clé). - SEO : réutiliser du contenu identique sur 50 pages peut créer de la duplication perçue. Utilisez plutôt des extraits, ou réutilisez des composants “structurels” (CTA, encarts) plutôt que des paragraphes entiers.
- Compatibilité future : restez sur les API core (WP_Query, REST, Transients). Évitez les accès directs SQL sauf cas extrêmes.
Ressources
- WP_Query (référence)
- Transients API (Handbook)
- add_shortcode()
- apply_filters()
- REST API Handbook
- WordPress core sur GitHub (wordpress-develop)
- WordPress Core Trac (tickets et historique)
- Déclarations de types PHP (php.net)
FAQ
Est-ce que ce plugin remplace les “Blocs réutilisables” (patterns/synced patterns) ?
Non. Les blocs réutilisables (et patterns synchronisés) sont parfaits quand vous contrôlez l’édition dans Gutenberg. Ici, on vise la réutilisation transversale (templates, builders, widgets) avec une API simple (shortcode/REST) et du cache.
Pourquoi ne pas utiliser directement get_template_part() ?
get_template_part() sert à réutiliser du template, pas du contenu éditorial. Si votre besoin est “réutiliser un composant de thème”, utilisez des template parts. Si votre besoin est “réutiliser une page existante”, ce plugin est plus adapté.
Le shortcode peut-il afficher un brouillon si je suis admin ?
Non, volontairement. J’ai vu des sites exposer des brouillons via un shortcode “pratique”. C’est une fuite de contenu qui finit souvent indexée ou partagée. Si vous voulez ce comportement en privé, ajoutez une option et un contrôle de capacité explicite.
Pourquoi votre invalidation de cache supprime tous les transients du plugin ?
Parce que sans index des clés, cibler “uniquement les transients liés à ce post” devient vite plus complexe qu’il n’y paraît. Sur un blog standard, cette purge est acceptable. Sur un gros site, passez à une stratégie de version de cache.
Est-ce compatible multisite ?
Oui, au sens où les transients et options sont stockés par site. Si vous avez un multisite avec object-cache persistant, c’est même mieux. La purge SQL reste locale au site courant (table options du site).
Pourquoi ne pas utiliser un bloc custom plutôt qu’un shortcode ?
Un bloc custom est plus propre côté éditeur, mais demande du build (block.json, scripts, bundling). Pour un besoin “intermédiaire” et une compatibilité page builders, le shortcode est souvent le choix le plus rentable.
Le contenu réutilisé n’a pas le bon CSS, pourquoi ?
Parce que vous réutilisez du HTML dans un contexte différent (footer, sidebar, modal). Le contenu peut dépendre de styles chargés uniquement sur certaines pages. Solution : déplacer/enqueue les styles globalement, ou simplifier le contenu réutilisé.
Je vois des shortcodes non interprétés dans le contenu réutilisé
Vérifiez que apply_content_filters="1". Si c’est déjà le cas, un plugin peut avoir désactivé do_shortcode dans the_content (rare, mais possible). Testez en désactivant temporairement les plugins de performance/éditeur.
Le REST renvoie 404 alors que la page existe
Deux causes typiques : permaliens non régénérés, ou page non publiée/privée. Régénérez les permaliens, puis vérifiez le statut du contenu. Et rappelez-vous : l’endpoint masque volontairement l’existence d’un contenu non visible.
Comment éviter la duplication SEO si je réutilise une section entière ?
Réutilisez des extraits, des CTA, des encarts, ou des listings dynamiques plutôt que des paragraphes identiques. Si vous devez réutiliser un texte légal, mettez-le sur une page dédiée et affichez un lien + extrait.
Peut-on ajouter des paramètres de requête (catégorie, tags) pour afficher une liste ?
Oui, mais là vous passez d’un “re-use” de contenu à un “query block”. Je recommande de créer un second shortcode dédié (ex. [wp_list]) avec une validation stricte des paramètres et une stratégie de cache séparée, plutôt que d’alourdir celui-ci.