Si vous avez déjà essayé d’embarquer un “mini-site” dans une page WordPress (documentation, outil interne, preview d’un autre site, sandbox) et que vous vous êtes retrouvé avec des boucles infinies, des CSS qui se marchent dessus ou des sessions qui sautent, vous avez touché le cœur du sujet : faire du “WordPress dans WordPress” sans casser l’hôte.
Le problème / Le besoin
“WordPress dans WordPress” recouvre plusieurs besoins réels que je vois souvent en maintenance :
- Afficher du contenu d’un autre WordPress (ou d’un autre environnement du même site) dans une page, un widget ou un module de page builder.
- Exposer une fonctionnalité WordPress en “micro-app” (ex : annuaire, base de connaissances, catalogue) sans re-développer un front complet.
- Isoler une zone (styles, scripts, cache, login) pour éviter que le thème principal (Divi/Avada) pollue l’expérience.
- Créer une vue “embed” stable pour l’intégration dans un intranet, un CRM, ou une page marketing.
Le piège classique, c’est de vouloir “inclure” WordPress comme on inclurait un fichier PHP. Ça marche parfois… jusqu’au jour où un plugin ajoute un hook global, où le cache full-page s’en mêle, ou où PHP dépasse la mémoire.
À la fin, vous saurez mettre en place une intégration propre basée sur l’API REST (et une variante iframe durcie), avec un plugin mu (ou plugin standard) copiable-collable, compatible WordPress 6.9.4 et PHP 8.1+.
Résumé rapide
- Vous allez créer un petit plugin qui consomme l’API REST d’un WordPress “source” et rend le contenu dans le WordPress “hôte” via un shortcode.
- Vous allez ajouter un endpoint REST côté source (optionnel) pour exposer exactement ce dont vous avez besoin (et rien de plus).
- Vous allez gérer cache (transients), sanitization et escaping pour éviter XSS et lenteurs.
- Vous aurez une variante iframe avec en-têtes de sécurité et paramètres URL propres, utile quand vous devez embarquer une UI complète.
- Vous verrez les adaptations pratiques pour Divi 5, Elementor et Avada.
Quand utiliser cette solution
- Vous devez afficher des posts/pages d’un autre WordPress (staging, multisite, site “headless” simple) dans votre site principal.
- Vous voulez un rendu cohérent (votre thème) mais des données externes (un autre WP, un sous-site, une base de connaissances).
- Vous devez limiter les risques : éviter d’exécuter du PHP d’un autre WordPress dans le même process.
- Vous avez besoin de contrôle sur le cache et la performance (l’API REST + transients est très prévisible).
- Vous travaillez avec un page builder et vous voulez une intégration “bloc” (shortcode / widget HTML) plutôt qu’un hack dans le thème.
Quand ne PAS utiliser cette solution
- Vous voulez “tout le site dans le site” avec navigation complète, formulaires, login, panier, etc. Là, l’API REST devient vite un mini-projet headless. Une iframe ou un sous-domaine dédié sera plus réaliste.
- Vous contrôlez les deux sites et vous pouvez fusionner proprement (multisite, ou migration). C’est souvent plus simple à long terme.
- Vous avez besoin de SEO sur le contenu embarqué mais vous ne pouvez pas le rendre côté serveur. Un embed iframe ne transmet pas le SEO comme une page native.
- Vous manipulez des données sensibles (profil, commandes). Préférez une intégration SSO/ OAuth et des pages dédiées, pas un “embed” rapide.
Prérequis / avant de commencer
- WordPress 6.9.4+ sur les deux sites (hôte et source) et PHP 8.1+.
- Un environnement de test (staging) et une sauvegarde. Je vois encore des gens coller un snippet sur production, provoquer une erreur fatale, puis se rendre compte que l’accès FTP est verrouillé.
- Accès admin sur les deux WordPress (ou au moins la capacité d’ajouter un plugin côté source).
- Un plugin de snippets est possible, mais je recommande un vrai plugin (ou mu-plugin) pour éviter les “snippets cassés” après mise à jour de thème.
Outils utiles :
- Un plugin de logs (ou
WP_DEBUG_LOG) pour capturer les erreurs HTTP. - Un outil pour tester l’API REST : curl, Postman, ou juste le navigateur.
Docs officielles que vous allez utiliser en cours de route :
L’approche naïve (et pourquoi l’éviter)
Le réflexe que je croise le plus : “je vais inclure le wp-load.php de l’autre site et appeler WP_Query”. Exemple typique (à ne pas faire) :
<?php
// ❌ Exemple dangereux : ne faites pas ça.
require_once '/var/www/autre-site/wp-load.php';
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 5,
]);
while ($q->have_posts()) {
$q->the_post();
echo '<h3>' . get_the_title() . '</h3>';
}
wp_reset_postdata();
?>
Pourquoi ça casse en conditions réelles :
- Conflits globaux : constantes, hooks, plugins, autoloaders. Deux WordPress chargés dans le même process PHP, c’est une loterie.
- Sessions / cookies : collisions de noms de cookies, problèmes de login, redirections inattendues.
- Performance : vous chargez deux stacks WordPress complètes pour afficher 5 titres.
- Sécurité : vous ouvrez une surface d’attaque énorme (et une simple erreur de chemin devient un vecteur).
- Maintenance : à la première mise à jour d’un plugin “dans l’autre WP”, vous cassez l’hôte.
Le bon réflexe, c’est d’échanger des données via une interface stable : REST (ou RSS si vous êtes minimaliste), puis de rendre le HTML dans l’hôte.
La bonne approche — tutoriel pas à pas
Objectif technique
Vous allez :
- Créer côté “source” un endpoint REST public et minimal qui renvoie une liste de contenus (titre, lien, extrait) au format JSON.
- Créer côté “hôte” un plugin avec un shortcode
[wpdanswp]qui appelle l’endpoint, met en cache la réponse, et affiche une liste HTML propre. - Ajouter une variante “embed iframe” si vous devez intégrer une UI complète.
Étape 1 — Côté source : exposer un endpoint REST dédié
Oui, WordPress expose déjà /wp-json/wp/v2/posts. Mais dans la vraie vie, je préfère souvent un endpoint dédié : plus stable, plus léger, et vous contrôlez exactement les champs (et donc la surface XSS).
Sur le WordPress “source”, créez un plugin (ou mu-plugin) :
- Plugin classique :
wp-content/plugins/wp-dans-wp-source/wp-dans-wp-source.php - MU plugin :
wp-content/mu-plugins/wp-dans-wp-source.php
<?php
/**
* Plugin Name: WP dans WP (Source) - Endpoint Embed
* Description: Expose un endpoint REST minimal pour embarquer du contenu dans un autre WordPress.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
*/
defined('ABSPATH') || exit;
add_action('rest_api_init', function () {
// Endpoint public, mais volontairement limité.
register_rest_route('wpdanswp/v1', '/feed', [
'methods' => 'GET',
'callback' => 'wpdanswp_source_feed',
'permission_callback' => '__return_true',
'args' => [
'type' => [
'description' => 'Type de contenu (post, page, ou CPT)',
'type' => 'string',
'required' => false,
'sanitize_callback' => 'sanitize_key',
'default' => 'post',
],
'per_page' => [
'description' => 'Nombre d’éléments (max 20)',
'type' => 'integer',
'required' => false,
'default' => 5,
'sanitize_callback' => 'absint',
],
],
]);
});
function wpdanswp_source_feed(WP_REST_Request $request): WP_REST_Response {
$type = $request->get_param('type') ?: 'post';
$per_page = (int) ($request->get_param('per_page') ?: 5);
// Limites strictes : évite les abus (scraping agressif, charge serveur).
if ($per_page < 1) {
$per_page = 1;
}
if ($per_page > 20) {
$per_page = 20;
}
// Autoriser uniquement des post types publics.
$post_type_obj = get_post_type_object($type);
if (!$post_type_obj || empty($post_type_obj->public)) {
return new WP_REST_Response([
'error' => 'post_type_not_allowed',
], 400);
}
$q = new WP_Query([
'post_type' => $type,
'posts_per_page' => $per_page,
'post_status' => 'publish',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]);
$items = [];
foreach ($q->posts as $post) {
// Préparer des champs sûrs et stables.
$items[] = [
'id' => (int) $post->ID,
'title' => html_entity_decode(get_the_title($post), ENT_QUOTES, 'UTF-8'),
'url' => get_permalink($post),
'excerpt' => wp_strip_all_tags(get_the_excerpt($post)),
'date' => get_the_date(DATE_W3C, $post),
];
}
$response = new WP_REST_Response([
'site' => [
'name' => get_bloginfo('name'),
'url' => home_url('/'),
],
'items' => $items,
], 200);
// Cache côté client (CDN / navigateur) : utile si l’endpoint est public.
$response->header('Cache-Control', 'public, max-age=300');
return $response;
}
Test rapide dans le navigateur :
https://source.example/wp-json/wpdanswp/v1/feed?type=post&per_page=5
Étape 2 — Côté hôte : créer le plugin d’affichage (shortcode)
Sur le WordPress “hôte”, créez un plugin : wp-content/plugins/wp-dans-wp-host/wp-dans-wp-host.php.
Ce plugin :
- appelle l’endpoint via
wp_remote_get() - met en cache la réponse via un transient
- rend une liste HTML échappée
- expose un shortcode utilisable dans Gutenberg et les page builders
Étape 3 — (Optionnel) Variante iframe pour une UI complète
Quand vous devez embarquer une page entière (recherche, filtres, UI riche), l’API REST ne suffit plus. Dans ce cas, une iframe est acceptable si vous durcissez :
- les en-têtes (CSP frame-ancestors / X-Frame-Options selon votre stratégie)
- les paramètres (whitelist) et le sandbox
- le cache et les cookies (éviter le login croisé)
Code complet
1) Plugin “source” (endpoint REST minimal)
<?php
/**
* Plugin Name: WP dans WP (Source) - Endpoint Embed
* Description: Expose un endpoint REST minimal pour embarquer du contenu dans un autre WordPress.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
*/
defined('ABSPATH') || exit;
add_action('rest_api_init', function () {
register_rest_route('wpdanswp/v1', '/feed', [
'methods' => 'GET',
'callback' => 'wpdanswp_source_feed',
'permission_callback' => '__return_true',
'args' => [
'type' => [
'description' => 'Type de contenu (post, page, ou CPT)',
'type' => 'string',
'required' => false,
'sanitize_callback' => 'sanitize_key',
'default' => 'post',
],
'per_page' => [
'description' => 'Nombre d’éléments (max 20)',
'type' => 'integer',
'required' => false,
'default' => 5,
'sanitize_callback' => 'absint',
],
],
]);
});
function wpdanswp_source_feed(WP_REST_Request $request): WP_REST_Response {
$type = $request->get_param('type') ?: 'post';
$per_page = (int) ($request->get_param('per_page') ?: 5);
if ($per_page < 1) {
$per_page = 1;
}
if ($per_page > 20) {
$per_page = 20;
}
$post_type_obj = get_post_type_object($type);
if (!$post_type_obj || empty($post_type_obj->public)) {
return new WP_REST_Response([
'error' => 'post_type_not_allowed',
], 400);
}
$q = new WP_Query([
'post_type' => $type,
'posts_per_page' => $per_page,
'post_status' => 'publish',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]);
$items = [];
foreach ($q->posts as $post) {
$items[] = [
'id' => (int) $post->ID,
'title' => html_entity_decode(get_the_title($post), ENT_QUOTES, 'UTF-8'),
'url' => get_permalink($post),
'excerpt' => wp_strip_all_tags(get_the_excerpt($post)),
'date' => get_the_date(DATE_W3C, $post),
];
}
$response = new WP_REST_Response([
'site' => [
'name' => get_bloginfo('name'),
'url' => home_url('/'),
],
'items' => $items,
], 200);
$response->header('Cache-Control', 'public, max-age=300');
return $response;
}
2) Plugin “hôte” (shortcode + cache + variante iframe)
<?php
/**
* Plugin Name: WP dans WP (Host) - Embed via REST/iframe
* Description: Récupère du contenu d’un autre WordPress via REST et l’affiche via shortcode. Inclut une variante iframe durcie.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
*/
defined('ABSPATH') || exit;
/**
* Shortcode principal :
* [wpdanswp url="https://source.example/wp-json/wpdanswp/v1/feed" type="post" per_page="5" cache="300" title="Derniers articles"]
*/
add_shortcode('wpdanswp', 'wpdanswp_host_shortcode');
function wpdanswp_host_shortcode($atts): string {
$atts = shortcode_atts([
'url' => '',
'type' => 'post',
'per_page' => 5,
'cache' => 300,
'title' => '',
], $atts, 'wpdanswp');
$endpoint = esc_url_raw($atts['url']);
if (!$endpoint) {
return '<div class="wpdanswp-error">Endpoint manquant. Ajoutez l’attribut <code>url</code> au shortcode.</div>';
}
$type = sanitize_key($atts['type']);
$per_page = absint($atts['per_page']);
$cache_ttl = absint($atts['cache']);
if ($per_page < 1) {
$per_page = 1;
}
if ($per_page > 20) {
$per_page = 20;
}
if ($cache_ttl < 30) {
// Évite de marteler l’API en cas de page très visitée.
$cache_ttl = 30;
}
if ($cache_ttl > 3600) {
$cache_ttl = 3600;
}
// Construire l’URL avec query args.
$url = add_query_arg([
'type' => $type,
'per_page' => $per_page,
], $endpoint);
$cache_key = 'wpdanswp_' . md5($url);
$data = get_transient($cache_key);
if (!is_array($data)) {
$response = wp_remote_get($url, [
'timeout' => 8,
'redirection' => 3,
'headers' => [
'Accept' => 'application/json',
],
]);
if (is_wp_error($response)) {
return '<div class="wpdanswp-error">Erreur HTTP : ' . esc_html($response->get_error_message()) . '</div>';
}
$code = (int) wp_remote_retrieve_response_code($response);
$body = (string) wp_remote_retrieve_body($response);
if ($code < 200 || $code >= 300) {
return '<div class="wpdanswp-error">Endpoint inaccessible (HTTP ' . esc_html((string) $code) . ').</div>';
}
$decoded = json_decode($body, true);
if (!is_array($decoded) || empty($decoded['items']) || !is_array($decoded['items'])) {
return '<div class="wpdanswp-error">Réponse JSON invalide ou vide.</div>';
}
// Normaliser les données attendues, et ignorer le reste.
$data = [
'site' => [
'name' => isset($decoded['site']['name']) ? (string) $decoded['site']['name'] : '',
'url' => isset($decoded['site']['url']) ? (string) $decoded['site']['url'] : '',
],
'items' => [],
];
foreach ($decoded['items'] as $item) {
if (!is_array($item)) {
continue;
}
$title = isset($item['title']) ? (string) $item['title'] : '';
$permalink = isset($item['url']) ? esc_url_raw((string) $item['url']) : '';
$excerpt = isset($item['excerpt']) ? (string) $item['excerpt'] : '';
$date = isset($item['date']) ? (string) $item['date'] : '';
if (!$permalink) {
continue;
}
$data['items'][] = [
'title' => $title,
'url' => $permalink,
'excerpt' => $excerpt,
'date' => $date,
];
}
set_transient($cache_key, $data, $cache_ttl);
}
$heading = trim((string) $atts['title']);
if ($heading === '') {
$heading = $data['site']['name'] ? ('Contenu de ' . $data['site']['name']) : 'Contenu embarqué';
}
// Rendu HTML : tout échapper, pas de HTML provenant de la source.
$out = '<div class="wpdanswp" data-source="' . esc_attr($endpoint) . '">';
$out .= '<div class="wpdanswp__header"><strong>' . esc_html($heading) . '</strong></div>';
$out .= '<ul class="wpdanswp__list">';
foreach ($data['items'] as $item) {
$out .= '<li class="wpdanswp__item">';
$out .= '<a class="wpdanswp__link" href="' . esc_url($item['url']) . '" target="_blank" rel="noopener">' . esc_html($item['title']) . '</a>';
if (!empty($item['date'])) {
$out .= '<br><span class="wpdanswp__date">' . esc_html(wpdanswp_host_format_date($item['date'])) . '</span>';
}
if (!empty($item['excerpt'])) {
$out .= '<br><span class="wpdanswp__excerpt">' . esc_html($item['excerpt']) . '</span>';
}
$out .= '</li>';
}
$out .= '</ul>';
$out .= '</div>';
return $out;
}
/**
* Formatage date : accepte DATE_W3C, renvoie une date localisée.
*/
function wpdanswp_host_format_date(string $w3c): string {
$ts = strtotime($w3c);
if (!$ts) {
return $w3c;
}
// wp_date() respecte la locale et le fuseau configuré.
return wp_date(get_option('date_format'), $ts);
}
/**
* Shortcode iframe :
* [wpdanswp_iframe src="https://source.example/embed/" height="800"]
*/
add_shortcode('wpdanswp_iframe', 'wpdanswp_host_iframe_shortcode');
function wpdanswp_host_iframe_shortcode($atts): string {
$atts = shortcode_atts([
'src' => '',
'height' => 700,
'title' => 'Contenu embarqué',
], $atts, 'wpdanswp_iframe');
$src = esc_url_raw($atts['src']);
if (!$src) {
return '<div class="wpdanswp-error">Attribut <code>src</code> manquant pour l’iframe.</div>';
}
$height = absint($atts['height']);
if ($height < 200) {
$height = 200;
}
if ($height > 2000) {
$height = 2000;
}
// Sandbox : adaptez selon vos besoins. Évitez allow-same-origin sauf nécessité.
$sandbox = 'allow-forms allow-scripts allow-popups';
$html = '<div class="wpdanswp-iframe">';
$html .= '<iframe';
$html .= ' src="' . esc_url($src) . '"';
$html .= ' title="' . esc_attr((string) $atts['title']) . '"';
$html .= ' loading="lazy"';
$html .= ' referrerpolicy="no-referrer-when-downgrade"';
$html .= ' sandbox="' . esc_attr($sandbox) . '"';
$html .= ' style="width:100%;height:' . esc_attr((string) $height) . 'px;border:0;"';
$html .= '></iframe>';
$html .= '</div>';
return $html;
}
/**
* Un peu de CSS minimal côté front.
* Vous pouvez aussi l’intégrer dans votre thème enfant.
*/
add_action('wp_enqueue_scripts', function () {
$css = '
.wpdanswp { padding: 12px; border: 1px solid rgba(0,0,0,.08); border-radius: 10px; }
.wpdanswp__header { margin-bottom: 8px; }
.wpdanswp__list { margin: 0; padding-left: 18px; }
.wpdanswp__item { margin: 0 0 10px 0; }
.wpdanswp__date { opacity: .75; font-size: .92em; }
.wpdanswp__excerpt { display: inline-block; margin-top: 4px; opacity: .9; }
.wpdanswp-error { padding: 12px; border-left: 4px solid #cc1818; background: rgba(204,24,24,.06); }
';
wp_register_style('wpdanswp-inline', false, [], '1.0.0');
wp_enqueue_style('wpdanswp-inline');
wp_add_inline_style('wpdanswp-inline', $css);
}, 20);
Explication du code
Pourquoi REST plutôt que “include WP”
REST découple complètement les deux WordPress. Le site hôte ne charge pas les plugins du site source. Il consomme un JSON stable. En dépannage, c’est le jour et la nuit : vous pouvez tester l’endpoint seul, mesurer le temps de réponse, et isoler un problème réseau.
Côté source : endpoint minimal, champs contrôlés
register_rest_route()déclare une route stable :/wp-json/wpdanswp/v1/feed. Doc : register_rest_route().permission_callbackest public (__return_true) volontairement. Si vous avez du contenu privé, changez ce point (voir variantes).- Validation : on limite
per_pageà 20 pour éviter les abus. J’ai déjà vu un site tomber parce qu’un embed demandait 200 items sur une home très visitée. - Performance :
no_found_rows+ caches meta/terms désactivés = requête plus légère. - Sécurité XSS : on renvoie des champs texte (titre, extrait) sans HTML. C’est un choix. Vous pouvez enrichir, mais ça demande une stratégie stricte de whitelist HTML.
Côté hôte : HTTP API + cache
wp_remote_get()est la bonne API : support proxy, SSL, erreurs WP_Error. Doc : wp_remote_get().- Transients : on cache la réponse. Sans ça, une page builder qui ré-affiche le module 3 fois peut déclencher 3 appels réseau par page, multipliés par le trafic.
- Échappement : tout ce qui sort est échappé via
esc_html(),esc_url(),esc_attr(). Le JSON est traité comme non fiable. - Normalisation : on reconstruit un tableau
$data“propre” et on ignore les champs inattendus. Ça évite des surprises quand la source évolue.
Variante iframe : sandbox et limites
L’iframe est utile, mais c’est une surface de risques :
- si vous mettez
allow-same-origin, le contenu peut se comporter comme “même origine” dans certains cas (selon la combinaison). Je ne l’active que si je sais exactement pourquoi. - si la source envoie des en-têtes anti-iframe (X-Frame-Options / CSP), votre embed sera bloqué. C’est normal.
Variantes et cas d’usage
Variante 1 — Endpoint protégé par clé (simple, pas parfait)
Pour un site “semi-privé”, vous pouvez exiger une clé via query arg. Ce n’est pas un OAuth, mais c’est déjà un filtre anti-scraping.
Côté source, ajoutez un paramètre key et vérifiez-le :
<?php
// Dans l’args de register_rest_route :
'key' => [
'description' => 'Clé d’accès',
'type' => 'string',
'required' => false,
'sanitize_callback' => 'sanitize_text_field',
],
// Puis dans le callback :
$expected = (string) get_option('wpdanswp_shared_key', '');
$provided = (string) $request->get_param('key');
if ($expected !== '' && !hash_equals($expected, $provided)) {
return new WP_REST_Response(['error' => 'forbidden'], 403);
}
Attention : une clé dans l’URL peut fuiter via logs, referers, analytics. Pour du sensible, passez à une auth robuste (Application Passwords, OAuth, ou proxy serveur).
Variante 2 — Rendre du HTML “autorisé” (whitelist)
Si vous devez afficher un extrait avec un peu de HTML (liens, <em>, <strong>), utilisez wp_kses() côté hôte. Ne faites pas confiance au HTML source.
<?php
$allowed = [
'a' => ['href' => true, 'target' => true, 'rel' => true],
'em' => [],
'strong' => [],
'br' => [],
];
$safe_excerpt_html = wp_kses($item['excerpt_html'] ?? '', $allowed);
Variante 3 — Multi-endpoints et fallback WordPress natif
Quand je dois intégrer plusieurs sources (ex : blog, base de connaissances, changelog), je garde le shortcode unique et je passe des endpoints différents, avec fallback sur /wp-json/wp/v2/posts si l’endpoint custom n’existe pas.
Techniquement, vous pouvez détecter une réponse 404 et basculer. Gardez juste en tête que wp/v2 renvoie des champs plus complexes (et parfois du HTML).
Compatibilité Divi 5 / Elementor / Avada
Divi 5
Dans Divi 5, le plus robuste est d’utiliser un module Code ou un module Texte avec le shortcode :
[wpdanswp url="https://source.example/wp-json/wpdanswp/v1/feed" type="post" per_page="5" cache="300" title="À lire aussi"]
Piège Divi fréquent : si vous voyez le shortcode affiché en texte brut, activez l’option d’interprétation des shortcodes dans le module (selon le module), ou utilisez un module “Shortcode” si disponible.
Elementor
Avec Elementor, utilisez le widget Shortcode. Même shortcode, même rendu. Si Elementor minifie/concatène agressivement, pensez à purger le cache après activation du plugin.
Avada (Fusion Builder)
Avada accepte les shortcodes dans plusieurs éléments (Text Block, Code Block). En pratique, je conseille le Code Block pour éviter que l’éditeur ne ré-écrive des guillemets.
CSS et thèmes builders
Divi/Avada injectent beaucoup de styles globaux. Le CSS fourni dans le plugin est volontairement minimal. Si votre liste est “écrasée”, ajoutez des classes plus spécifiques dans votre thème enfant, pas dans le builder (sinon vous perdez la traçabilité).
Vérifications après mise en place
- Testez l’endpoint source directement dans le navigateur : vous devez obtenir un JSON avec
siteetitems. - Sur le site hôte, ajoutez le shortcode dans une page simple (pas votre homepage) et vérifiez l’affichage.
- Vérifiez le cache : rechargez 5 fois, puis regardez le temps de réponse (DevTools). Vous ne devriez pas voir d’appel réseau côté navigateur, car l’appel REST est côté serveur.
- Si vous utilisez un cache full-page (plugin/serveur/CDN), purge après ajout du shortcode. J’ai souvent vu un “ça marche pas” qui était juste un HTML mis en cache avant activation du plugin.
Tableau de diagnostic rapide
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| “Endpoint manquant” | Attribut url absent ou mal formé |
Inspectez le shortcode dans la page | Ajoutez url="https://.../wp-json/wpdanswp/v1/feed" |
| “Endpoint inaccessible (HTTP 403)” | Firewall/WAF, restriction REST, clé requise | Testez l’URL via curl | Autorisez l’endpoint, ajustez permission_callback, ajoutez la clé |
| “Réponse JSON invalide” | Réponse HTML (maintenance mode, redirection), ou JSON cassé | Ouvrez l’endpoint dans le navigateur | Désactivez le plugin “maintenance”, corrigez la redirection, vérifiez SSL |
| Temps de chargement lent | Pas de cache, timeout trop élevé, endpoint lent | Mesurez TTFB de l’endpoint | Augmentez TTL transient, optimisez WP_Query côté source |
| Shortcode affiché en texte | Widget/module n’exécute pas les shortcodes | Testez dans l’éditeur de page natif | Utilisez widget “Shortcode” (Elementor) / module adapté (Divi/Avada) |
Si ça ne marche pas
1) Vérifiez que le code est au bon endroit
- Le plugin est-il bien dans
wp-content/plugins/...et activé ? - Si vous avez collé ça dans
functions.phpdu thème, attention aux mises à jour (et aux erreurs fatales qui rendent l’admin inaccessible).
2) Vérifiez les erreurs PHP (classique)
Une parenthèse oubliée ou un point-virgule manquant vous donnera une page blanche ou une erreur 500. Activez temporairement :
<?php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Puis consultez wp-content/debug.log.
3) Testez l’endpoint en ligne de commande
curl -i "https://source.example/wp-json/wpdanswp/v1/feed?type=post&per_page=5"
Si vous voyez une redirection 301/302 vers une page de login ou une page HTML, votre JSON ne sera jamais valide côté hôte.
4) Problèmes SSL / certificats
Les erreurs SSL sont fréquentes en staging. Si wp_remote_get() échoue et que curl fonctionne, regardez les certificats intermédiaires. Ne désactivez pas la vérification SSL “pour que ça marche”. Corrigez le SSL.
5) Cache et permaliens
- Purger cache plugin + cache serveur + CDN.
- Si l’endpoint custom ne répond pas, regénérez les permaliens côté source (Réglages > Permaliens > Enregistrer). Ça corrige parfois des règles réécrites par des plugins.
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
| Copier le code dans le mauvais fichier | Snippet collé dans un template, ou dans le mauvais site (source vs hôte) | Créer deux plugins distincts, nommés clairement, et vérifier l’activation |
Fatal error: Cannot redeclare ... |
Fonction dupliquée (code collé deux fois, ou plugin + functions.php) | Ne garder qu’une seule définition, préfixer les fonctions (wpdanswp_...) |
Erreur HTTP : cURL error 28 |
Timeout (endpoint lent, DNS, firewall) | Optimiser l’endpoint, réduire le payload, augmenter légèrement timeout si nécessaire |
| Hook inadapté côté source | Route déclarée hors rest_api_init |
Enregistrer la route dans add_action('rest_api_init', ...) |
| Conflit avec cache full-page | La page est servie depuis un HTML ancien | Purger cache, réduire TTL transient, invalider cache à la publication (variante avancée) |
| JS/CSS non chargé | Mauvais hook d’enqueue, ou builder qui isole le CSS | Utiliser wp_enqueue_scripts, vérifier priorité, purger minification |
| Code d’un vieux tutoriel | Utilisation d’anciennes pratiques (include wp-load, echo non échappé) | Passer REST + escaping systématique (compatible WP 6.9.4 / PHP 8.1) |
| Test direct en production | Absence de staging/sauvegarde | Tester sur staging, puis déployer (ou au minimum sauvegarde + rollback) |
Conseils sécurité, performance et maintenance
Sécurité
- Traitez le JSON comme non fiable. Même si vous contrôlez la source, un plugin compromis peut injecter du contenu malveillant.
- Échappez tout en sortie. Si vous devez afficher du HTML, utilisez
wp_kses()avec une whitelist stricte. - Limitez l’endpoint (per_page, post_types autorisés). Un endpoint public non limité devient une API de scraping gratuite.
- Iframe : utilisez
sandbox, évitezallow-same-originsans raison, et vérifiez la politique CSP côté source.
Performance
- Cache transient côté hôte. C’est le gain le plus net.
- Endpoint léger côté source : pas de meta/term cache, pas de pagination lourde, payload minimal.
- CDN : l’en-tête
Cache-Controlaide si vous avez un reverse proxy devant la source.
Maintenance
- Versionnez vos plugins (même en interne). Un petit
1.0.1avec changelog évite les “on a changé un truc, on ne sait plus quoi”. - Surveillez les logs côté hôte : les erreurs réseau apparaissent souvent après un changement DNS/SSL.
- Évitez les dépendances thème : gardez l’embed dans un plugin, pas dans un builder ou un template.
Ressources
- REST API Handbook (WordPress)
- wp_remote_get() (HTTP API)
- register_rest_route()
- Transients API (set_transient)
- wp_kses() (filtrage HTML)
- WordPress core sur GitHub (wordpress-develop)
- WordPress Core Trac (suivi des tickets)
- json_decode() (PHP)
FAQ
Peut-on faire “WordPress dans WordPress” sans API REST ?
Oui, via RSS/Atom (simple) ou iframe (UI complète). Mais dès que vous voulez contrôler les champs, le cache, et la sécurité, REST reste l’option la plus propre.
Pourquoi ne pas utiliser directement /wp-json/wp/v2/posts ?
Vous pouvez. Mais vous récupérez plus de données, parfois du HTML, et vous dépendez d’un schéma plus général. Un endpoint dédié est plus stable et plus facile à durcir.
Est-ce que ça marche en multisite ?
Oui. En multisite, vous pouvez avoir une “source” sur un sous-site et l’hôte sur un autre. Gardez simplement des URLs correctes (domaine + chemin), et attention aux restrictions REST ajoutées par certains plugins réseau.
Comment invalider le cache automatiquement quand un article est publié sur la source ?
Le plus simple reste un TTL court (300s). Pour une invalidation push, il faut soit un webhook (source → hôte), soit une stratégie de cache key versionnée. C’est faisable, mais ça dépasse le “copier-coller” sans infrastructure.
Mon endpoint est public : est-ce dangereux ?
Ça dépend de ce que vous exposez. Si vous ne renvoyez que des contenus déjà publics, le risque principal est la charge (scraping). Limitez per_page, mettez du cache, et surveillez.
Pourquoi ne pas afficher le HTML complet du contenu source ?
Parce que c’est là que les soucis XSS et CSS commencent. Si vous devez afficher du HTML, passez par wp_kses() avec whitelist, et acceptez que certains shortcodes/blocs ne se rendront pas pareil sur l’hôte.
Est-ce compatible avec Divi 5 / Elementor / Avada ?
Oui, parce que l’intégration se fait via shortcodes. Les trois builders savent les rendre via un widget/module adapté. Le point d’attention est le cache (purge) et les styles globaux.
Pourquoi mon iframe est vide alors que l’URL marche seule ?
Très souvent : en-tête X-Frame-Options: SAMEORIGIN ou CSP frame-ancestors côté source. Vérifiez dans l’onglet Network/Console du navigateur.
Je vois des caractères bizarres dans les titres (accents, guillemets)
En général, c’est un problème d’encodage ou d’entités HTML. Côté source, le code décode le titre en UTF-8. Si votre source injecte des entités non standard, normalisez avant l’envoi.
Est-ce que je peux embarquer des contenus privés (réservés aux membres) ?
Possible, mais il faut une authentification côté REST (Application Passwords ou OAuth) et un proxy côté serveur. Ne mettez pas une “clé secrète” dans une page publique si le contenu est sensible.
Quel est le meilleur choix : REST ou iframe ?
REST si vous voulez des données et un rendu natif (SEO, styles de votre thème, cache maîtrisé). Iframe si vous devez embarquer une application complète et isoler les styles/scripts, en acceptant les limites SEO et les contraintes d’en-têtes.