SEO on-page et contenu

Maillage interne sous 100 pages : une méthode

Une structure en étoile pour petit site, notre règle des 3 liens dans le texte et un script Node qui repère les pages orphelines et celles liées par le menu seul.

Vue aérienne d'un échangeur autoroutier à plusieurs niveaux
Photo: Timo Volz sur Pexels

Avant la refonte, nous avons lancé le script de ce guide sur greenlightmysite.com. Le site comptait 14 URL et aucune page orpheline : chacune recevait un lien d’au moins une autre page. Pourtant, 6 des 14 (les pages À propos, confidentialité et conditions d’utilisation, en anglais et en français) n’étaient accessibles que par le menu et le pied de page, jamais depuis une phrase du corps d’une page. Un contrôle qui demande seulement « cette page est-elle liée quelque part ? » valide ce site. La bonne question sépare les liens que porte chaque page de ceux que quelqu’un a choisi d’écrire.

Ce que Google affirme sur les liens, et ce qui n’est que notre habitude

La documentation de Google sur les liens explorables pose 3 points qui comptent ici. Seul un élément <a> muni d’un href est exploré comme un lien : un span, ou un élément qui n’a qu’un gestionnaire onclick, ne compte pas. Le texte d’ancre doit être descriptif et concis, ce qui exclut « cliquez ici ». Enfin, chaque page importante doit recevoir au moins 1 lien d’une autre page. Google ne donne aucun nombre cible de liens par page ni par site. Tout ce qui ressemble à un quota est donc notre propre pratique, et nous l’étiquetons comme telle.

<!-- Explorable : un vrai lien avec une destination -->
<a href="/guides/robots-txt-mistakes/">robots.txt mistakes that block AdSense</a>

<!-- Non explorés comme liens -->
<span onclick="location.href='/guides/robots-txt-mistakes/'">robots.txt mistakes</span>
<a onclick="openGuide('robots')">robots.txt mistakes</a>

Un sitemap ne remplace pas les liens. Google le présente comme un simple indice, sans garantie, et une page découverte uniquement par ce biais arrive sans texte d’ancre, sans phrase autour et sans signe que vous la considérez comme faisant partie du site. Sur un site de moins de 100 pages, une page sans lien est presque toujours une page oubliée, pas une page cachée par choix.

Une structure en étoile pour moins de 100 pages

Avec moins de 100 pages, inutile de concevoir une architecture de liens. Il vous faut une forme que vous tenez en tête. Nous utilisons l’étoile. Chaque catégorie a une page centrale qui liste tous ses guides. Chaque guide est un satellite : il renvoie vers sa page centrale et, sur le côté, vers 2 ou 3 guides voisins. Les outils se placent à côté des pages centrales, et tout guide qui explique ce qu’un outil mesure renvoie vers cet outil. Les pages centrales ont rarement besoin de se lier entre elles, puisque l’accueil et le menu le font déjà.

Voici le câblage de ce site, car le gabarit fait une partie du travail et vous devez savoir laquelle. Chaque guide porte un fil d’Ariane (accueil, guides, catégorie) et une étiquette de catégorie, tous deux liés vers la page centrale. La colonne latérale propose la table des matières du guide, l’outil gratuit le plus pertinent, 3 guides liés et la liste des catégories avec leur nombre de guides. Sous l’article, une rangée de 3 guides liés reprend l’idée. Le gabarit choisit d’abord des guides de la même catégorie, puis les plus récents : il garantit une structure, pas la pertinence. Les liens qui portent du sens sont ceux que nous écrivons à la main dans le texte.

La publication par vagues ajoute une difficulté. Dans notre Markdown, un lien s’écrit sous forme de clé, par exemple guide:sitemap, et la construction du site ne le transforme en vrai lien qu’à partir de la date de publication du guide visé. D’ici là, le texte reste et le lien disparaît : aucune page ne pointe vers une 404 entre deux vagues, et les liens écrits en avance s’activent seuls. Le cas inverse n’est pas couvert : quand vous publiez une nouvelle page, les anciennes, écrites avant elle, ignorent son existence. Après chaque vague, cherchez le nouveau sujet dans vos pages existantes (grep -ril "sitemap" content/ suffit) et ajoutez les liens à la main.

Notre règle des 3 liens dans le texte, et l’écriture des ancres

Chaque guide de ce site contient au moins 3 liens dans son corps de texte vers d’autres pages du site, dont au moins 1 vers un outil quand le sujet est quelque chose qu’un outil mesure. C’est notre pratique, pas une règle de Google. Ce nombre fonctionne comme une limitation de vitesse : il nous force à nous demander, pour chaque page, où le lecteur va ensuite. Sur un site de 30 pages, un guide qui ne sait pas nommer 3 destinations voisines est soit hors sujet pour le site, soit privé des liens dont il a besoin.

Les liens dans le texte comptent parce que ceux du menu, du pied de page et de la colonne latérale apparaissent sur toutes les pages. Ils disent « cette page existe » et rien de sa pertinence. Un lien au milieu d’une phrase dit « voici la page à lire sur X », et son ancre nomme X. Voici un exemple de réécriture, avec des phrases inventées et non extraites d’une vraie page. Le script plus bas rend mesurable la différence entre liens du gabarit et liens du texte.

AvantAprès
Si votre fichier robots pose problème, cliquez ici pour en savoir plus.Un Disallow: / hérité de la préproduction est la cause la plus courante, et le guide des erreurs de robots.txt montre comment le repérer.
Notre autre article en parle.Les URL à écarter sont détaillées dans le guide du sitemap XML.

Ces réécritures suivent 4 habitudes. Placez le lien là où la question se pose, pas dans une liste « pour aller plus loin » en fin de page. Nommez la destination dans l’ancre. Liez la première mention d’une page et laissez les suivantes en texte simple. Et n’employez jamais la même ancre pour 2 pages différentes : le lecteur comme le robot n’auraient plus aucun moyen de les distinguer.

Repérer les pages orphelines et les pages liées par le menu seul, avec un script

Une page orpheline est une URL de votre sitemap qu’aucune autre page ne cite. Une page « menu seul » est une page que les autres pages ne lient que depuis le menu, l’en-tête, le pied de page ou la colonne latérale. Le script ci-dessous trouve les deux. Il lit les lignes Sitemap: de robots.txt (en se rabattant sur /sitemap.xml), suit les index de sitemaps, récupère chaque page listée 4 à la fois et compte, pour chacune, les liens reçus d’autres pages : une fois au total (« any »), puis une fois après retrait des blocs nav, header, footer et aside (« body »). Il demande Node 18 ou plus récent et aucun paquet. Enregistrez-le sous orphans.mjs. Ses messages restent en anglais, et les colonnes « any » et « body » correspondent à « au total » et « dans le texte ».

// orphans.mjs : liste les pages du sitemap qu'aucune autre page ne cite. Node 18+, aucune dépendance.
// Usage : node orphans.mjs https://example.com/
const site = new URL(process.argv[2] ?? 'https://example.com/');
const get = (u) => fetch(u, { signal: AbortSignal.timeout(15000), headers: { 'user-agent': 'orphan-check/1.0' } });
const clean = (u) => { u.hash = ''; return u.href; };

// 1. Collecte toutes les URL du sitemap (d'abord robots.txt, puis /sitemap.xml ; suit les index de sitemaps).
async function sitemapUrls() {
  const robots = await get(new URL('/robots.txt', site)).then((r) => (r.ok ? r.text() : ''), () => '');
  const queue = [...robots.matchAll(/^sitemap:\s*(\S+)/gim)].map((m) => m[1]);
  if (!queue.length) queue.push(new URL('/sitemap.xml', site).href);
  const pages = new Set(), seen = new Set();
  while (queue.length) {
    const next = queue.shift();
    if (seen.has(next)) continue;
    seen.add(next);
    const res = await get(next);
    if (!res.ok) { console.error(`Cannot read ${next} (HTTP ${res.status})`); continue; }
    const xml = await res.text();
    const locs = [...xml.matchAll(/<loc>\s*([^<\s]+)\s*<\/loc>/g)].map((m) => m[1]);
    if (/<sitemapindex/i.test(xml)) queue.push(...locs); else locs.forEach((l) => pages.add(clean(new URL(l))));
  }
  return [...pages];
}

// 2. Lit les liens <a href> d'une page. « Body » = liens restants une fois nav, header, footer et aside retirés.
function linksOf(html, base) {
  const out = (h) => new Set([...h.matchAll(/<a\s[^>]*?href\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>/gi)]
    .filter((m) => !/rel\s*=\s*["'][^"']*nofollow/i.test(m[0]))
    .map((m) => (m[1] ?? m[2]).replace(/&amp;/g, '&').trim())
    .filter((h) => h && !/^(#|mailto:|tel:|javascript:)/i.test(h))
    .map((h) => { try { return new URL(h, base); } catch { return null; } })
    .filter((u) => u && u.origin === site.origin)
    .map(clean));
  const body = html.replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, '');
  const stripped = body.replace(/<(nav|header|footer|aside)\b[\s\S]*?<\/\1>/gi, '');
  return { all: out(body), contextual: out(stripped) };
}

const pages = await sitemapUrls();
const inbound = new Map(pages.map((p) => [p, { all: new Set(), contextual: new Set() }]));
const outsideSitemap = new Map();

// 3. Récupère chaque page, 4 à la fois, et note qui lie vers qui.
let cursor = 0;
await Promise.all(Array.from({ length: 4 }, async () => {
  while (cursor < pages.length) {
    const page = pages[cursor++];
    const res = await get(page).catch(() => null);
    if (!res || !res.ok || !/html/.test(res.headers.get('content-type') ?? '')) { console.error(`Skipped ${page}`); continue; }
    const { all, contextual } = linksOf(await res.text(), page);
    for (const t of all) {
      if (t === page) continue;
      if (inbound.has(t)) { inbound.get(t).all.add(page); if (contextual.has(t)) inbound.get(t).contextual.add(page); }
      else if (!/\.(css|js|svg|png|jpe?g|webp|avif|ico|xml|txt|json)$/i.test(t)) outsideSitemap.set(t, page);
    }
  }
}));

// 4. Rapport.
const rows = [...inbound].map(([url, v]) => ({ url: url.replace(site.origin, ''), any: v.all.size, body: v.contextual.size }))
  .sort((a, b) => a.body - b.body || a.any - b.any);
console.log(`${pages.length} URLs in the sitemap of ${site.origin}\n`);
console.log('any / body   page   (inbound links from other pages)');
for (const r of rows) console.log(`${String(r.any).padStart(3)} / ${String(r.body).padEnd(5)} ${r.url}`);
const orphans = rows.filter((r) => r.any === 0), weak = rows.filter((r) => r.any > 0 && r.body === 0);
console.log(`\nOrphans (no link from any other page): ${orphans.length}`);
console.log(`Menu-only (linked, but never from page content): ${weak.length}`);
if (outsideSitemap.size) {
  console.log(`Linked internally but missing from the sitemap: ${outsideSitemap.size}`);
  for (const [url, from] of outsideSitemap) console.log(`  ${url.replace(site.origin, '')}  (seen on ${from.replace(site.origin, '')})`);
}
process.exitCode = orphans.length ? 1 : 0;

Nous l’avons testé le 7 octobre 2026 sur l’ancienne version de ce site, puis sur un site local jetable contenant une page orpheline plantée, une page « menu seul », un lien nofollow et un lien vers une URL absente du sitemap. Il a trouvé les 4. Voici la vraie sortie pour greenlightmysite.com :

14 URLs in the sitemap of https://greenlightmysite.com

any / body   page   (inbound links from other pages)
  7 / 0     /about/
  7 / 0     /fr/about/
  7 / 0     /fr/privacy/
  7 / 0     /fr/terms/
  7 / 0     /privacy/
  7 / 0     /terms/
  7 / 1     /contact/
  7 / 1     /fr/contact/
  7 / 1     /fr/guide/
  7 / 1     /fr/seo-checker/
  7 / 1     /guide/
  7 / 1     /seo-checker/
  7 / 2     /
  7 / 2     /fr/

Orphans (no link from any other page): 0
Menu-only (linked, but never from page content): 6

Le script a des limites à connaître avant de croire un zéro. Il lit le HTML statique : les liens que JavaScript construit après le chargement lui sont invisibles (Google sait exécuter JavaScript, pas ce script). Il analyse le code avec des expressions régulières, pas avec un vrai analyseur HTML, si bien qu’un gabarit inhabituel peut demander d’ajuster la règle nav|header|footer|aside. Il compare les URL telles qu’elles sont écrites : une barre oblique finale manquante peut donc produire une fausse orpheline, à vérifier en premier quand un résultat vous surprend. Enfin, il envoie 4 requêtes en parallèle, ce qui reste poli sur votre propre site et ne se dirige pas vers celui d’un autre.

Que faire de chaque résultat

Pour une page orpheline, demandez-vous d’abord si elle mérite d’exister. Une page que personne n’a liée est parfois une page dont personne n’avait besoin, et le guide pour repérer les contenus de faible valeur ajoutée aide à trancher. Si elle reste, liez-la depuis 3 pages qui traitent du même sujet et depuis sa page de catégorie, puis relancez le script pour confirmer.

Pour une page « menu seul », ajoutez 2 ou 3 liens dans le texte depuis des pages où un lecteur voudrait la trouver. Sur notre propre liste, nous avons lu le résultat comme une tâche à faire pour la page À propos, qui a quelque chose à dire aux lecteurs de presque tous les guides, et comme acceptable pour la confidentialité et les conditions : un lien dans le texte vers une page de conditions serait du remplissage, et le remplissage est précisément ce que la règle veut éviter. La fiche auteur sous chaque guide renvoie désormais vers la page À propos depuis l’intérieur de l’article.

Pour une page liée mais absente du sitemap, ajoutez-la au sitemap ou retirez le lien. Le guide du sitemap explique ce qui y a sa place. Le côté page par page est couvert par le vérificateur SEO : il compte les liens internes de la page que vous lui donnez (notre seuil de réussite est de 3 ou plus), signale les ancres génériques comme « cliquez ici » et teste jusqu’à 12 liens internes pour repérer les impasses. La structure des titres dans chaque page est un chantier à part, traité dans le guide sur la structure des titres.

Réponses rapides

Faut-il parfois mettre rel=nofollow sur un lien interne ?

Pas sur un lien éditorial ordinaire. Les valeurs nofollow, sponsored et ugc servent à qualifier des liens dont vous ne voulez pas vous porter garant. Notre script ignore volontairement les liens nofollow : une page accessible uniquement par eux apparaît donc comme orpheline.

Les liens du pied de page comptent-ils comme des liens internes ?

Ce sont des liens explorables, donc ils permettent de découvrir une page. Mais ils figurent sur toutes les pages, avec la même ancre partout, et ne disent rien des pages qui se rapprochent par le sujet. C'est pourquoi le script compte à part les liens du corps de texte.

Sources

Qui est derrière ce site

GreenlightMySite est tenu par Stéphane Cardon, ingénieur réseaux et cybersécurité. Nous écrivons à partir de vrais sites passés par de vraies candidatures, et chaque affirmation technique est soit liée à une source primaire, soit mesurée avec nos propres outils.

À propos · Politique éditoriale · LinkedIn