From f4ea1639f2df537636454ed977437bff60d6b916 Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Thu, 8 Oct 2026 17:07:37 +0200 Subject: [PATCH] =?UTF-8?q?chore(outils):=20script=20de=20contr=C3=B4le=20?= =?UTF-8?q?des=20liens,=20outil=20de=20revue=20(B6-M4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet Claude-Session: https://claude.ai/code/session_0136n5xMLHuaBDLcPJYCDjRW --- scripts/check-liens-outils.mjs | 91 ++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 scripts/check-liens-outils.mjs diff --git a/scripts/check-liens-outils.mjs b/scripts/check-liens-outils.mjs new file mode 100644 index 0000000..72c57e2 --- /dev/null +++ b/scripts/check-liens-outils.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * Contrôle des liens de /outils (B6-M4) — outil de revue, PAS un test : ne fait jamais échouer le build. + * Lit public/data/outils.json (toutes les URL, y compris `urls[]` et `episodes[].url`) et + * public/data/fmhy-curated.json, fait un GET (redirections suivies, délai 15 s, 4 requêtes en parallèle) + * et liste les réponses hors 2xx/3xx. + * + * Un 403/429 d'un site qui bloque les robots n'est pas un lien mort : lire la liste avant de retirer quoi que ce soit. + * Usage : node scripts/check-liens-outils.mjs + */ + +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const lire = (nom) => JSON.parse(readFileSync(join(__dirname, '..', 'public', 'data', nom), 'utf8')) + +const DELAI_MS = 15000 +const PARALLELE = 4 +const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36' + +/** url -> Set des entrées qui la portent. Toute chaîne https:// est prise, quel que soit son champ. */ +const urls = new Map() +function ajoute(url, qui) { + if (!urls.has(url)) urls.set(url, new Set()) + urls.get(url).add(qui) +} + +/** Titre lisible de l'entrée courante : nom / titre / mot, sinon l'id. */ +const titreDe = (o) => o.nom ?? o.titre ?? o.name ?? o.mot ?? o.id + +function parcourt(valeur, fichier, contexte) { + if (typeof valeur === 'string') { + if (/^https:\/\//i.test(valeur) || /^http:\/\//i.test(valeur)) ajoute(valeur, `${fichier} › ${contexte}`) + } else if (Array.isArray(valeur)) { + for (const v of valeur) parcourt(v, fichier, contexte) + } else if (valeur && typeof valeur === 'object') { + // `lien_texte`-like chaînes ne commencent pas par https:// : seules les vraies URL sont retenues. + const ctx = titreDe(valeur) ? `${contexte ? contexte + ' › ' : ''}${titreDe(valeur)}` : contexte + for (const [, v] of Object.entries(valeur)) parcourt(v, fichier, ctx) + } +} + +parcourt(lire('outils.json'), 'outils.json', '') +parcourt(lire('fmhy-curated.json'), 'fmhy-curated.json', '') + +async function verifie(url) { + const ctrl = new AbortController() + const minuteur = setTimeout(() => ctrl.abort(), DELAI_MS) + try { + const r = await fetch(url, { + method: 'GET', + redirect: 'follow', + signal: ctrl.signal, + headers: { 'user-agent': UA, accept: 'text/html,application/xhtml+xml,*/*;q=0.8', 'accept-language': 'fr-FR,fr;q=0.9,en;q=0.7' }, + }) + r.body?.cancel().catch(() => {}) + return r.status + } catch (e) { + return e?.name === 'AbortError' ? 'délai' : `erreur (${e?.cause?.code ?? e?.message ?? 'réseau'})` + } finally { + clearTimeout(minuteur) + } +} + +const liste = [...urls.keys()] +const resultats = new Map() +let curseur = 0 +async function travailleur() { + while (curseur < liste.length) { + const url = liste[curseur++] + resultats.set(url, await verifie(url)) + } +} +await Promise.all(Array.from({ length: PARALLELE }, travailleur)) + +const ko = liste.filter((u) => { + const c = resultats.get(u) + return !(typeof c === 'number' && c >= 200 && c < 400) +}) + +console.log(`Contrôle de ${liste.length} URL (GET, délai ${DELAI_MS / 1000} s, ${PARALLELE} en parallèle)\n`) +if (ko.length) { + console.log('code'.padEnd(18) + 'URL · entrée') + for (const u of ko) { + console.log(String(resultats.get(u)).padEnd(18) + `${u} · ${[...urls.get(u)].join(' | ')}`) + } +} +console.log(`\n${ko.length} réponse(s) hors 2xx/3xx sur ${liste.length} URL.`) +// Outil de revue : toujours code de sortie 0.