Rebuild du site sur Astro : i18n multilingue, faits verifies, Docker, CI, tests
- Astro statique, EN par defaut + /fr/ + /nl/, detection langue navigateur - i18n par fichiers JSON, ajouter une langue = ajouter des traductions - Contenu original porte a l'identique (diff d'inventaire par langue) - Chronologie legislative corrigee sur sources primaires (PE, Conseil, votes) - Timeline 23 precedents + observatoire 35 items + annuaire 66 outils FOSS, chaque affirmation verifiee et sourcee - Zero requete tierce (teste), lisible JS coupe, axe WCAG AA x2 themes - Version offline monofichier par langue a chaque build - Docker multi-stage vers nginx + CSP stricte auto-generee - CI GitHub Actions (SHA-pinnees) + verification hebdo des liens - CONTRIBUTING : divulgation d'affiliation obligatoire, allowlist de domaines testee en CI
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
/* 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)
|
||||
Reference in New Issue
Block a user