On a aligné deux environnements clones d’un site WooCommerce, même jeu de données, même hébergement. D’un côté un plugin de breadcrumb du répertoire officiel, de l’autre un snippet PHP de 35 lignes glissé dans le thème enfant. Le LCP en environnement contrôlé a baissé de façon sensible, et les ressources réseau inutiles se sont évaporées. Voilà pourquoi intégrer un fil d’Ariane sans plugin n’est pas un luxe de puriste : c’est un bras de levier direct sur les signaux d’expérience utilisateur et sur la clarté du crawl.
Pourquoi un fil d’Ariane ? Et pourquoi sans plugin ?
Un breadcrumb correctement structuré sert trois objectifs. D’abord, il donne à l’internaute un repère spatial immédiat sur un site profond, sans le forcer à cliquer sur le logo pour repartir de zéro. Ensuite, il redistribue le PageRank interne en pointant vers les pages mères de la hiérarchie, ce qui renforce les silos thématiques. Enfin, il offre à Google un signal de structure qu’il peut afficher dans les SERP à la place de l’URL, ce qui améliore le CTR.
Alors pourquoi se passer d’un plugin ? Parce que les extensions de breadcrumb du répertoire embarquent presque toujours une couche CSS, parfois un script de suivi des clics, et des options d’administration qui chargent des assets même sur les pages publiques. Sur une installation standard avec un thème correctement codé, un fil d’Ariane peut être produit en pur PHP, sans aucune requête SQL additionnelle si l’on exploite les fonctions natives de WordPress. Moins de code chargé, c’est un TTFB plus bas et un LCP qui arrive plus vite, surtout sur mobile.
La structure HTML et les données structurées : un duo indissociable
Si vous voulez que Google affiche le chemin dans les résultats, il ne suffit pas d’une suite de liens dans une <div>. Il faut embarquer des microdonnées schema.org de type BreadcrumbList directement dans le HTML. Un balisage JSON-LD injecté dans le <head> peut fonctionner, mais l’approche la plus robuste reste la microdonnée inline, car elle ne dépend pas de l’exécution d’un script externe et elle est lue sans ambiguïté par Googlebot.
Voici la structure cible pour un article de blog dans la catégorie « SEO technique » :
<nav aria-label="Fil d'Ariane">
<ol itemscope itemtype="https://schema.org/BreadcrumbList">
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="https://example.com/">
<span itemprop="name">Accueil</span>
</a>
<meta itemprop="position" content="1" />
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="https://example.com/seo-technique/">
<span itemprop="name">SEO technique</span>
</a>
<meta itemprop="position" content="2" />
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<span itemprop="name">Fil d'Ariane sans plugin</span>
<meta itemprop="position" content="3" />
</li>
</ol>
</nav>
Ce balisage respecte les exigences de Google pour l’affichage du breadcrumb dans les SERP, à condition que chaque URL soit canonique et que le chemin reflète la hiérarchie réelle du site. Pas besoin de JSON-LD en doublon si les microdonnées sont correctes.
Le code PHP pas à pas : récupérer les ancêtres et construire le chemin
Place au concret. Ouvre le fichier functions.php de ton thème enfant, ou mieux, crée un fichier breadcrumb.php que tu incluras avec get_template_part(). On commence par une fonction qui retourne le balisage complet.
function rm_breadcrumb() {
if ( is_front_page() ) return;
$separator = ' › ';
$output = '<nav aria-label="Fil d\'Ariane"><ol itemscope itemtype="https://schema.org/BreadcrumbList">';
$position = 1;
// Accueil
$output .= sprintf(
'<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><a itemprop="item" href="%s"><span itemprop="name">Accueil</span></a><meta itemprop="position" content="%d"/></li>%s',
esc_url( home_url( '/' ) ), $position, $separator
);
$position++;
if ( is_singular() ) {
$post_id = get_queried_object_id();
$ancestors = get_post_ancestors( $post_id );
if ( ! empty( $ancestors ) ) {
$ancestors = array_reverse( $ancestors );
foreach ( $ancestors as $ancestor_id ) {
$output .= sprintf(
'<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><a itemprop="item" href="%s"><span itemprop="name">%s</span></a><meta itemprop="position" content="%d"/></li>%s',
esc_url( get_permalink( $ancestor_id ) ),
esc_html( get_the_title( $ancestor_id ) ),
$position, $separator
);
$position++;
}
}
// Élément courant
$output .= sprintf(
'<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><span itemprop="name">%s</span><meta itemprop="position" content="%d"/></li>',
esc_html( get_the_title( $post_id ) ), $position
);
}
// Cas des catégories, tags, etc.
elseif ( is_category() ) {
$category = get_queried_object();
if ( $category->parent ) {
$parent_ids = get_ancestors( $category->term_id, 'category' );
$parent_ids = array_reverse( $parent_ids );
foreach ( $parent_ids as $parent_id ) {
$parent = get_category( $parent_id );
$output .= sprintf(
'<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><a itemprop="item" href="%s"><span itemprop="name">%s</span></a><meta itemprop="position" content="%d"/></li>%s',
esc_url( get_category_link( $parent_id ) ),
esc_html( $parent->name ), $position, $separator
);
$position++;
}
}
$output .= sprintf(
'<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><span itemprop="name">%s</span><meta itemprop="position" content="%d"/></li>',
esc_html( $category->name ), $position
);
}
// Fallback archives
elseif ( is_archive() && ! is_category() ) {
$output .= sprintf(
'<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><span itemprop="name">%s</span><meta itemprop="position" content="%d"/></li>',
esc_html( get_the_archive_title() ), $position
);
}
$output .= '</ol></nav>';
return $output;
}
Place ensuite <?php echo rm_breadcrumb(); ?> dans header.php ou dans le template de contenu, juste avant le <main>. Aucune requête SQL manuelle, tout passe par les fonctions de cache objet natif de WordPress. Le temps d’exécution reste sous la milliseconde.
Cas particuliers : pages statiques, articles de blog, catégories et produits WooCommerce
Le bloc précédent couvre les pages simples et les catégories. Pour un site e-commerce sous WooCommerce, on veut afficher « Accueil › Boutique › Catégorie produit › Produit ». La bonne nouvelle, c’est que get_post_ancestors() fonctionne sur les produits, et que get_the_terms() permet de récupérer les catégories de produit. On peut enrichir la fonction avec une condition if ( is_singular('product') ).
Plutôt que d’alourdir le snippet avec des switch interminables, on peut structurer des inclusions conditionnelles. Pour une page statique sans parent, le breadcrumb se limite à « Accueil › Titre de la page ». Pour un article de blog classé dans plusieurs catégories, on affiche la première catégorie parente disponible, celle avec la priorité la plus haute dans le tableau retourné par get_the_category(). Éviter de boucler sur toutes les catégories : le breadcrumb doit désigner un chemin unique.
⚠️ Attention : si un article appartient à deux catégories de même niveau, choisissez une règle fixe (la première par ordre alphabétique ou la catégorie principale définie par Yoast) et documentez-la. Un breadcrumb flottant entre deux chemins déroute l’utilisateur et dilue le signal de hiérarchie.
Pour les custom post types, il suffit de vérifier is_singular('votre_cpt') et d’ajouter un lien vers la page d’archive correspondante si elle existe. Dans tous les cas, le code reste confiné dans le thème enfant, sans dépendance externe.
Impact sur les Core Web Vitals : ce que le code manuel retire de la page
Le gain n’est pas théorique. Un plugin de breadcrumb « tout-en-un » charge typiquement une feuille de style de 5 à 10 Ko, parfois une police d’icônes via Font Awesome, et un fichier JavaScript pour la gestion des clics ou du responsive. Sur une page où le LCP est une image de produit au-dessus de la ligne de flottaison, ces ressources entrent en compétition avec l’image principale et retardent le premier affichage.
En supprimant ce poids mort, le navigateur traite moins de requêtes et le fil de rendu critique s’allège. Le breadcrumb maison, lui, est rendu côté serveur dans le flux HTML initial. Il ne nécessite aucun CSS additionnel si l’on s’appuie sur les styles du thème, et il n’exécute aucun JavaScript. Le LCP ne subit plus de contention réseau liée à un composant secondaire.
On a mesuré l’écart sur une page produit type avec un thème de base et un hébergement mutualisé milieu de gamme. Le breadcrumb codé main abaissait le LCP de quelques dixièmes de seconde, assez pour franchir les seuils verts de la Search Console. Ce n’est pas une révolution, c’est la somme de micro-optimisations qui fait basculer un rapport Core Web Vitals du orange au vert.
Intégrer le snippet dans un thème enfant sans casser les mises à jour
C’est la règle d’or : jamais de modification dans le thème parent. Place le fichier breadcrumb.php dans le dossier du thème enfant et appelle-le avec get_template_part('breadcrumb'). Si le thème parent propose un hook juste avant la zone de contenu, branche-toi dessus pour injecter le breadcrumb sans toucher aux templates. Sinon, copie le header.php du parent dans l’enfant et insère l’appel.
Teste toujours en activant temporairement le thème parent pour vérifier que le breadcrumb ne disparaît pas en cas de désactivation accidentelle de l’enfant. Une alternative propre consiste à créer un plugin de fonction (un mu-plugin) avec le code du breadcrumb. L’avantage, c’est que le breadcrumb survit à un changement de thème. L’inconvénient, c’est qu’il faut y placer la logique d’affichage, donc lier le code au hook wp_body_open ou à un add_action spécifique, ce qui rajoute une couche d’abstraction.
💡 Conseil : si vous gérez plusieurs sites WordPress, regroupez la fonction breadcrumb dans un mu-plugin maison versionné. Vous capitalisez sur un seul code audité, et vous le branchez en deux lignes sur chaque projet.
Vérification et suivi : outils de test des données structurées et Search Console
Une fois le snippet en production, passe chaque type de page (article, page, catégorie, produit) dans l’outil de test des données structurées de Google ou dans le validateur Schema Markup. Vérifie que le type BreadcrumbList est détecté et que chaque position est bien incrémentée de 1 sans saut. Contrôle aussi l’attribut item qui doit pointer vers une URL canonique absolue.
Dans la Search Console, le rapport d’amélioration « Données structurées » liste les erreurs et avertissements. Un avertissement courant est la présence d’un position qui ne commence pas à 1, souvent causé par une condition mal placée. Une autre erreur classique : l’utilisation de site_url() au lieu de home_url(), ce qui peut ajouter un sous-répertoire superflu quand WordPress est installé dans un dossier. Corrige vite, Google envoie une notification en cas d’erreur sur les données structurées, et laisser une erreur non résolue trop longtemps peut affecter l’éligibilité aux résultats enrichis.
Questions fréquentes
Peut-on utiliser ce snippet avec un thème FSE (Full Site Editing) ?
Oui, en transformant la fonction PHP en un bloc dynamique personnalisé via register_block_type(). L’appel à rm_breadcrumb() devient alors disponible dans l’éditeur de site. L’avantage, c’est que l’emplacement du breadcrumb reste piloté visuellement, sans perdre le contrôle du code.
Faut-il désactiver le breadcrumb généré par Yoast SEO avant d’utiliser ce code ?
Absolument. Si Yoast injecte déjà un bloc JSON-LD BreadcrumbList, un doublon se crée. Désactivez l’option dans Yoast (ou filtrez le hook wpseo_breadcrumb_output) et laissez le snippet maison prendre le relais. De cette manière, les microdonnées sont inline et le JSON-LD redondant disparaît.
Comment gérer le breadcrumb sur le blog principal et les pages d’archive de type custom ?
Pour la page des articles, ajoutez une condition is_home() qui insère le titre « Blog » avant l’éventuelle pagination. Pour les custom post types, ajoutez un lien vers l’URL de l’archive (get_post_type_archive_link()) avant le post. Le principe reste le même : un chemin unique, sans embranchement multiple.