J’ai automatisé le maillage interne de mon site WordPress avec N8N et un service RAG maison. Résultat : trois workflows tournent en autonomie, chaque nouvel article reçoit ses liens internes dans les 24h, et les pages orphelines remontent mécaniquement grâce à un algorithme de boost. Voici comment ça fonctionne vraiment, les problèmes rencontrés en prod, et ce que j’aurais fait différemment.

Pourquoi automatiser le maillage interne sur WordPress ?

Sur un site de plus de 80 articles répartis en silos thématiques, maintenir le maillage interne manuellement est impossible à tenir dans le temps. Chaque nouvel article devrait pointer vers 3 à 5 articles existants du même silo, et chaque article existant devrait recevoir de nouveaux liens au fil des publications. En pratique, sans automatisation, les nouvelles pages restent isolées et les articles anciens ne bénéficient jamais des contenus récents.

J’ai mis en prod ce système en janvier 2025 sur wordpress-freelance.com. Cinq mois plus tard, le service tourne toujours sans intervention manuelle. Ce que ça a changé concrètement : les articles publiés en milieu de semaine ont leurs liens internes intégrés dès le lendemain matin, et les pages qui avaient moins de 3 liens entrants internes ont été rattrapées progressivement par le workflow de rattrapage hebdomadaire.

Quelle est l’architecture technique du système de maillage ?

Le système repose sur quatre couches : WordPress (la source des contenus), un service RAG FastAPI en Python (la recherche sémantique), Claude Sonnet (la génération des ancres), et N8N (l’orchestration). Postgres joue deux rôles : stocker les embeddings vectoriels et tenir un ledger des ancres déjà placées pour éviter la répétition.

Le service RAG tourne sur le VPS en localhost port 8001 et expose trois endpoints : /index_article pour créer ou mettre à jour un vecteur d’embedding à chaque publication, /search_similar pour retrouver les articles du même silo sémantiquement proches, et /inlinks_batch pour récupérer le nombre de liens internes follow pointant vers chaque URL candidate.

La séquence complète pour chaque article traité :

  1. N8N récupère le contenu HTML brut via l’API WordPress
  2. Il résout la catégorie racine (silo) en remontant la hiérarchie des catégories
  3. Il interroge le RAG avec le contenu complet, le filtre de silo, et les IDs à exclure
  4. Il consulte le ledger Postgres pour récupérer les ancres déjà utilisées vers chaque URL candidate
  5. Il envoie le tout à Claude avec les contraintes : 3 liens exacts, ancres copiées caractère par caractère depuis le HTML, uniquement dans des balises <p>
  6. Il injecte les liens dans le HTML et met à jour l’article via l’API REST WordPress
  7. Il enregistre les ancres placées dans le ledger

Comment fonctionne la recherche sémantique par silo ?

La règle de silo est non négociable dans ce système : un article du silo « SEO Technique » ne peut recevoir des liens que d’articles du même silo. Pour l’appliquer mécaniquement, chaque article est indexé avec son silo racine calculé en remontant la hiérarchie WordPress via /wp-json/wp/v2/categories. La requête /search_similar prend un paramètre silo_filter qui limite les candidats au silo de l’article source.

Le score de similarité minimum est fixé à 0.55. En dessous, les candidats sont écartés même s’ils sont dans le bon silo. Dans mon expérience, 0.55 est le bon plancher : en dessous, les suggestions de Claude restent techniquement valides mais manquent de pertinence contextuelle.

Comment éviter de sur-mailler les mêmes pages systématiquement ?

C’est le problème principal d’un système basé sur la similarité pure : les articles les plus populaires et les mieux maillés remontent toujours en tête des candidats. Les pages orphelines, celles qui en auraient le plus besoin, n’apparaissent jamais.

J’ai résolu ça avec un algorithme de re-ranking à deux clés dans le module inlinks.py. Le principe : les candidats sont d’abord triés par bucket de similarité (tranches de 0.10), puis à l’intérieur de chaque bucket, les pages les moins maillées remontent grâce à un score boosté :

boosted_score = similarity × (1 + MAX_BOOST × deficit)
deficit = max(0, TARGET_INLINKS - inlinks_follow) / TARGET_INLINKS

Avec TARGET_INLINKS=10 et MAX_BOOST=0.6, une page à zéro liens entrants voit son score multiplié par 1.6 par rapport à son score de similarité brut. Mais elle ne peut jamais dépasser un article d’un bucket de similarité supérieur. La pertinence prime, le boost s’applique uniquement entre articles comparables.

Les données de maillage viennent d’exports RM Tech importés en Postgres via un script Python. C’est du batch, pas du temps réel, mais un refresh hebdomadaire suffit pour que l’algorithme reste pertinent.

Comment fonctionne la déduplication des ancres entre publications ?

Sans déduplication, le système placerait rapidement les mêmes ancres vers les mêmes cibles. Claude sélectionnerait naturellement les phrases les plus représentatives d’un article, et ces phrases seraient identiques d’une exécution à l’autre.

Le ledger anchor_ledger en Postgres enregistre chaque lien placé : article source, article cible, ancre exacte, ancre normalisée (lowercase + trim). Contrainte unique sur (source_id, target_url, anchor_norm). Avant chaque appel Claude, N8N interroge le ledger et passe les ancres déjà utilisées dans le prompt sous la forme d’une liste FORBIDDEN_ANCHORS par URL cible.

Deux règles complémentaires s’appliquent : une URL cible avec 12 liens entrants ou plus dans le ledger est exclue des candidats, et une ancre normalisée déjà utilisée vers une URL donnée ne peut pas être réutilisée pour ce même lien. En pratique, ça force Claude à trouver des formulations différentes et à varier les points d’entrée vers chaque article cible.

Quelles contraintes donner à Claude pour générer des ancres précises ?

La contrainte critique : l’ancre doit être une copie caractère par caractère d’une phrase qui existe exactement dans le HTML de l’article source. Pas de paraphrase, pas de reformulation. Si l’ancre n’est pas trouvée dans le HTML lors de l’injection, le lien est simplement ignoré.

Le prompt impose aussi : uniquement dans des balises <p> (jamais dans les <h2> ou <h3>), exactement 3 liens par article, des ancres de 2 à 6 mots, et une liste de mots interdits pour les ancres (cliquez ici, en savoir plus, essentiel, incontournable, meilleur, parfait, idéal). Claude reçoit les 8 000 premiers caractères du HTML source et les 7 meilleurs candidats avec leurs extraits et leurs ancres interdites.

Le taux de réussite en prod tourne autour de 85-90 % : sur 3 liens suggérés, 2.5 à 2.7 sont effectivement injectés en moyenne. Les échecs viennent principalement d’ancres légèrement paraphrasées par Claude malgré la consigne, ou de passages en entités HTML que la recherche exacte ne retrouve pas sans décodage préalable. J’ai ajouté un fallback avec décodage des entités HTML (&rsquo;, &eacute;, etc.) qui récupère une partie de ces cas.

Quels sont les trois workflows N8N et comment sont-ils organisés ?

Le workflow Daily (déclenchement 11h chaque jour) traite les articles publiés dans les 18 dernières heures. Il indexe d’abord chaque nouvel article dans le RAG, puis cible ceux qui ont moins de 6 liens internes et leur injecte 3 liens. C’est le workflow principal, celui qui couvre la quasi-totalité des cas normaux.

Le workflow Brute Force cible tous les articles du site ayant moins de 9 liens internes, sans fenêtre temporelle. Il passe l’intégralité du contenu en revue et est prévu pour les périodes de rattrapage ou après une refonte de silo. À utiliser ponctuellement : sur un site de 200 articles il peut générer des coûts API significatifs en un seul run.

Le workflow Rattrapage tourne bimensuellement le dimanche. Il ré-indexe d’abord les articles sous-maillés dans le RAG (pour mettre à jour les embeddings d’articles anciens dont le contenu a changé), puis leur cherche des candidats mis à jour. C’est lui qui assure la cohérence du système sur la durée, quand les contenus évoluent mais que les vecteurs ne reflètent plus l’état réel des articles.

Quelles erreurs ai-je faites et qu’est-ce que je referais autrement ?

Première erreur : indexer tous les articles à chaque run dans la première version. La facture Claude du premier mois m’a convaincu de cadrer strictement la fenêtre temporelle. Avec la fenêtre de 18h, la consommation de tokens est divisée par un facteur 10.

Deuxième erreur : ne pas avoir mis de contrainte de silo dès le début. Les premières suggestions de Claude étaient sémantiquement valides mais créaient des liens entre silos, ce qui cassait complètement l’architecture thématique du site. Ajouter le filtre de silo a nécessité de revoir la logique de résolution de catégorie, ce qui n’est pas trivial quand les catégories WordPress ont plusieurs niveaux de hiérarchie.

Troisième erreur : avoir sous-estimé la complexité du parsing HTML en prod. Les entités HTML, les emojis dans les titres, les guillemets typographiques — tout ça crée des cas limites que le remplacement exact ne gère pas bien. J’ai passé probablement 4 heures sur des bugs de ce type.

Ce que je referais différemment : implémenter le ledger d’ancres dès le début, pas après deux semaines de prod. Les premières exécutions ont créé des redondances qu’il a fallu nettoyer manuellement. Et paramétrer MAX_BOOST et TARGET_INLINKS via des variables d’environnement dès la conception, pour pouvoir les ajuster sans redéploiement.

Quel est l’impact concret sur le maillage interne du site ?

Avant le système, les articles publiés restaient souvent avec 0 à 2 liens internes entrants pendant plusieurs semaines. Après cinq mois de prod, les articles de moins de 6 liens entrants ont quasiment disparu du tableau de bord RM Tech. Le nombre moyen de liens internes par article a progressé, et surtout les pages orphelines ont été rattrapées par le workflow Rattrapage.

Ce système ne remplace pas une stratégie de maillage réfléchie. Les silos doivent être définis clairement en amont, les catégories WordPress bien structurées, et le contenu suffisamment dense pour que la recherche sémantique trouve des candidats pertinents. Avec un site mal architecturé, le système va mailler mécaniquement mais sans cohérence. Garbage in, garbage out : le RAG amplifie la structure existante, il ne la crée pas.