chore(outils): script de contrôle des liens, outil de revue (B6-M4)
Co-Authored-By: Claude Sonnet <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0136n5xMLHuaBDLcPJYCDjRW
This commit is contained in:
co-authored by
Claude Sonnet
parent
43ae065e26
commit
f4ea1639f2
@@ -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.
|
||||
Reference in New Issue
Block a user