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