Files
lage-chat-control/scripts/check-links.mjs
T

79 lines
2.6 KiB
JavaScript
Raw Normal View History

/* Live-checks every citation the guide stands on — timeline, observatory,
directory, checklist, allies, footer sources and the narrative sections.
404 / DNS failure / 5xx = FAIL; 403/429 (bot walls on europa.eu etc.)
are reported as WARN and tolerated.
Run: node scripts/check-links.mjs */
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { join } from 'node:path'
const UA =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'
// English sources are canonical — the other locales cite the same URLs.
const ROOTS = ['src/data', 'src/i18n/content/en', 'src/content/sections/en', 'src/components']
function filesUnder(root) {
try {
if (statSync(root).isFile()) return [root]
} catch {
return []
}
return readdirSync(root, { withFileTypes: true }).flatMap((e) =>
e.isDirectory() ? filesUnder(join(root, e.name)) : [join(root, e.name)],
)
}
const corpus = ROOTS.flatMap(filesUnder)
.map((p) => readFileSync(p, 'utf8'))
.join('\n')
const urls = [
...new Set(
[...corpus.matchAll(/https:\/\/[^\s"'`<>)\\]+/g)].map((m) => m[0].replace(/[.,]$/, '')),
),
]
let fail = 0
let warn = 0
async function probe(url) {
for (const method of ['HEAD', 'GET']) {
try {
const controller = new AbortController()
const t = setTimeout(() => controller.abort(), 15_000)
const res = await fetch(url, {
method,
redirect: 'follow',
signal: controller.signal,
headers: { 'user-agent': UA, accept: 'text/html,application/xhtml+xml,*/*' },
})
clearTimeout(t)
if (res.status === 405 && method === 'HEAD') continue // try GET
return res.status
} catch (e) {
if (method === 'GET')
return `ERR:${e.name === 'AbortError' ? 'timeout' : (e.cause?.code ?? e.name)}`
}
}
return 'ERR:unknown'
}
console.log(`checking ${urls.length} cited sources…`)
const results = await Promise.all(urls.map(async (url) => ({ url, status: await probe(url) })))
for (const { url, status } of results.sort((a, b) =>
String(a.status).localeCompare(String(b.status)),
)) {
if (typeof status === 'number' && status >= 200 && status < 300) {
console.log(`${status} ${url}`)
} else if (status === 403 || status === 429 || status === 401) {
warn++
console.log(` ~ ${status} ${url} (bot wall — verify by hand once)`)
} else {
fail++
console.log(`${status} ${url}`)
}
}
console.log(`\nlinks: ${urls.length - fail - warn} ok · ${warn} bot-walled · ${fail} broken`)
process.exit(fail === 0 ? 0 : 1)