From f8b824c540899c9b2961bd892db0f86f76739fe0 Mon Sep 17 00:00:00 2001 From: Richard Dern Date: Fri, 31 Oct 2025 12:41:34 +0100 Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20de=20la=20d=C3=A9tection=20?= =?UTF-8?q?de=20liens=20externes=20morts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/external_links.yaml | 3414 ----------------- tools/check_external_links.js | 611 ++- tools/config.json | 16 +- tools/lib/markdown_links.js | 246 ++ .../archive.test.js} | 2 +- tools/tests/markdown_links.test.js | 68 + .../puppeteer.test.js} | 2 +- 7 files changed, 885 insertions(+), 3474 deletions(-) delete mode 100644 data/external_links.yaml create mode 100644 tools/lib/markdown_links.js rename tools/{test_archive.js => tests/archive.test.js} (88%) create mode 100644 tools/tests/markdown_links.test.js rename tools/{test_puppeteer.js => tests/puppeteer.test.js} (87%) diff --git a/data/external_links.yaml b/data/external_links.yaml deleted file mode 100644 index 68bbd060..00000000 --- a/data/external_links.yaml +++ /dev/null @@ -1,3414 +0,0 @@ -https://fr.wikipedia.org/wiki/Réseau_de_diffusion_de_contenu: - status: 200 - checked: "2025-03-28T10:24:55.774Z" -https://portail.free.fr: - status: 200 - checked: "2025-03-28T10:24:56.673Z" -https://caddyserver.com/: - status: 200 - checked: "2025-03-28T10:24:58.616Z" -https://www.ovhcloud.com/fr/: - status: 200 - checked: "2025-03-28T10:24:59.519Z" -https://brickset.com/sets/8480-1: - status: 200 - checked: "2025-03-28T10:25:00.438Z" -https://www.ville-woippy.fr: - status: 200 - checked: "2025-03-28T10:25:01.890Z" -https://www.avenuedelabrique.com/associations-lego/cas-brick/37: - status: 200 - checked: "2025-03-28T10:25:03.013Z" -https://www.avenuedelabrique.com/expositions-lego/expo-lego-woippy-2024/451: - status: 200 - checked: "2025-03-28T10:25:03.836Z" -https://www.amazon.fr/LEGO-Jurassic-30390-Dinosaur-Polybag/dp/B09X6ZVVJL: - status: 200 - checked: "2025-03-28T10:25:07.831Z" -https://www.lego.com/fr-fr/themes/jurassic-world: - status: 200 - checked: "2025-03-28T10:25:10.218Z" -https://fr.wikipedia.org/wiki/Dilophosaurus#Dilophosaurus_dans_la_culture_populaire: - status: 200 - checked: "2025-03-28T10:25:11.790Z" -https://www.schleich-s.com/fr/FR/home: - status: 200 - checked: "2025-03-28T10:25:16.738Z" -https://www.joueclub.fr/909737/figurine-brachiosaure.html: - status: 200 - checked: "2025-03-28T10:25:18.405Z" -https://fr.wikipedia.org/wiki/Schleich: - status: 200 - checked: "2025-03-28T10:25:20.940Z" -https://fr.wikipedia.org/wiki/Acrocanthosaurus: - status: 200 - checked: "2025-03-28T10:25:23.639Z" -https://www.schleich-s.com/fr/FR/dinosaurs.html: - status: 200 - checked: "2025-03-28T10:25:25.893Z" -https://fr.wikipedia.org/wiki/Classification_scientifique_des_espèces: - status: 200 - checked: "2025-03-28T10:25:27.080Z" -https://fr.wikipedia.org/wiki/Anatomie_comparée: - status: 200 - checked: "2025-03-28T10:25:28.340Z" -https://en.wikipedia.org/wiki/Hell_Creek_Formation: - status: 200 - checked: "2025-03-28T10:25:29.102Z" -https://fr.wikipedia.org/wiki/Comparaison_des_agrégateurs_de_flux: - status: 200 - checked: "2025-03-28T10:25:30.298Z" -https://fr.wikipedia.org/wiki/RSS: - status: 200 - checked: "2025-03-28T10:25:31.021Z" -https://alternativeto.net/category/books--news/rss-feed-reader/: - status: 200 - checked: "2025-03-28T10:25:32.907Z" -https://fr.wikipedia.org/wiki/Neutralité_du_réseau: - status: 200 - checked: "2025-03-28T10:25:34.401Z" -https://www.amazon.fr/dp/B08VF4D2NT: - status: 404 - checked: "2025-03-28T10:25:35.083Z" -https://paypal.me/richarddern: - status: 200 - checked: "2025-03-28T10:25:37.403Z" -https://store.steampowered.com/wishlist/id/richarddern/#sort=order: - status: 429 - checked: "2025-03-28T10:25:38.764Z" -https://www.gog.com/fr/u/RichardDern/wishlist: - status: 200 - checked: "2025-03-28T10:25:40.348Z" -https://www.amazon.fr/hz/wishlist/ls/24XQEFC7L3GQB: - status: 200 - checked: "2025-03-28T10:25:42.013Z" -https://fr.wikipedia.org/wiki/Les_Sentinelles_de_l: - status: 404 - checked: "2025-03-28T10:25:43.305Z" -https://fr.wikipedia.org/wiki/Christopher_Nolan: - status: 200 - checked: "2025-03-28T10:25:44.152Z" -https://fr.wikipedia.org/wiki/Matthew_McConaughey: - status: 200 - checked: "2025-03-28T10:25:44.871Z" -https://www.nasa.gov/: - status: 200 - checked: "2025-03-28T10:25:46.134Z" -https://fr.wikipedia.org/wiki/Anne_Hathaway: - status: 200 - checked: "2025-03-28T10:25:47.704Z" -https://fr.wikipedia.org/wiki/Mackenzie_Foy: - status: 200 - checked: "2025-03-28T10:25:48.974Z" -https://fr.wikipedia.org/wiki/Jessica_Chastain: - status: 200 - checked: "2025-03-28T10:25:50.964Z" -https://fr.wikipedia.org/wiki/Michael_Caine: - status: 200 - checked: "2025-03-28T10:25:51.708Z" -https://fr.wikipedia.org/wiki/Kip_Thorne: - status: 200 - checked: "2025-03-28T10:25:53.102Z" -https://fr.wikipedia.org/wiki/Hans_Zimmer: - status: 200 - checked: "2025-03-28T10:25:53.861Z" -https://git.dern.ovh/Livres/reflexions/src/branch/main/03%20-%20Hasard%2C%20chaos/01-introduction.md: - status: 404 - checked: "2025-03-28T10:25:54.584Z" -https://consumer.huawei.com/fr/monitors/mateview-gt/: - status: 200 - checked: "2025-03-28T10:25:59.269Z" -https://github.com/ImminentFate/CompactGUI: - status: 200 - checked: "2025-03-28T10:26:01.051Z" -https://steamcommunity.com/app/858810/reviews/: - status: 200 - checked: "2025-03-28T10:26:02.393Z" -https://steamuserimages-a.akamaihd.net/ugc/1644339474302767879/7CD9311FCF009B3E9FAABFDC0500B6A0BCB16408/?imw=5000&imh=5000&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false: - status: 200 - checked: "2025-03-28T10:26:04.318Z" -https://steamcommunity.com/app/858810/discussions/0/3113647550043480990/: - status: 200 - checked: "2025-03-28T10:26:05.499Z" -https://steamcommunity.com/app/858810/workshop/: - status: 200 - checked: "2025-03-28T10:26:06.557Z" -https://pyramid.games/en/home/: - status: 200 - checked: "2025-03-28T10:26:08.440Z" -https://occupymarsgame.com: - status: 200 - checked: "2025-03-28T10:26:10.822Z" -https://store.steampowered.com/app/591460/Parkasaurus/: - status: 200 - checked: "2025-03-28T10:26:12.042Z" -https://store.steampowered.com/app/405820/Turok/: - status: 200 - checked: "2025-03-28T10:26:13.197Z" -https://fr.wikipedia.org/wiki/Guerre_des_os: - status: 200 - checked: "2025-03-28T10:26:16.107Z" -https://www.dwarffortresswiki.org/: - status: 200 - checked: "2025-03-28T10:26:17.567Z" -https://steamcommunity.com/id/richarddern/recommended/648350/: - status: 200 - checked: "2025-03-28T10:26:18.633Z" -https://store.steampowered.com/news/app/1244460/view/3126066060218586059: - status: 200 - checked: "2025-03-28T10:26:19.732Z" -https://www.youtube.com/watch?v=C7kbVvpOGdQ: - status: 200 - checked: "2025-03-28T10:26:21.896Z" -http://hellogames.org/: - status: 200 - checked: "2025-03-28T10:26:23.480Z" -https://nomanssky.fandom.com/wiki/No_Man%27s_Sky_Wiki: - status: 200 - checked: "2025-03-28T10:26:24.737Z" -https://www.twopointstudios.com/en/games/two-point-museum: - status: 200 - checked: "2025-03-28T10:26:25.940Z" -https://www.twopointstudios.com/: - status: 200 - checked: "2025-03-28T10:26:26.923Z" -https://www.twopointstudios.com/en/games/two-point-hospital: - status: 200 - checked: "2025-03-28T10:26:27.943Z" -https://www.ea.com/fr/games/theme/theme-hospital: - status: 200 - checked: "2025-03-28T10:26:36.881Z" -https://store.steampowered.com/app/2185060/Two_Point_Museum/: - status: 200 - checked: "2025-03-28T10:26:38.059Z" -https://fr.wikipedia.org/wiki/Wallace_et_Gromit: - status: 200 - checked: "2025-03-28T10:26:39.178Z" -https://books.apple.com/fr/book/jurassic-park-the-official-script-book/id6466819592: - status: 200 - checked: "2025-03-28T10:26:41.241Z" -https://www.amazon.fr/dp/B0C4JPNXKS: - status: 200 - checked: "2025-03-28T10:26:46.281Z" -https://en.wikipedia.org/wiki/Malia_Scotch_Marmo: - status: 200 - checked: "2025-03-28T10:26:47.553Z" -https://en.wikipedia.org/wiki/David_Koepp: - status: 200 - checked: "2025-03-28T10:26:48.255Z" -https://en.wikipedia.org/wiki/Phil_Tippett: - status: 200 - checked: "2025-03-28T10:26:48.946Z" -https://www.bingingwithbabish.com/recipes/chileanseabass: - status: 200 - checked: "2025-03-28T10:26:51.144Z" -https://en.wikipedia.org/wiki/Man_cave: - status: 200 - checked: "2025-03-28T10:26:51.859Z" -https://fr.wikipedia.org/wiki/Facteur_d%27acceptation_féminine: - status: 200 - checked: "2025-03-28T10:26:52.565Z" -https://en.wikipedia.org/wiki/Steffan_Andrews: - status: 200 - checked: "2025-03-28T10:26:53.392Z" -https://fr.wikipedia.org/wiki/Lost:_Missing_Pieces: - status: 200 - checked: "2025-03-28T10:26:54.096Z" -https://fr.wikipedia.org/wiki/The_Big_Bang_Theory#Distribution: - status: 200 - checked: "2025-03-28T10:26:56.673Z" -https://www.numerama.com/tech/1194882-jenna-ortega-ne-cligne-jamais-des-yeux-dans-wednesday-la-serie-netflix.html: - status: 200 - checked: "2025-03-28T10:26:57.839Z" -https://www.numerama.com/magazine/31075-revolv-achat-nest.html: - status: 200 - checked: "2025-03-28T10:26:59.623Z" -https://www.businessinsider.com/googles-nest-closing-smart-home-company-revolv-bricking-devices-2016-4?utm_source=feedly&utm_medium=referral?r=US&IR=T: - status: 200 - checked: "2025-03-28T10:27:01.573Z" -https://arlogilbert.com/the-time-that-tony-fadell-sold-me-a-container-of-hummus-cb0941c762c1#.pkjyhfklv: - status: 200 - checked: "2025-03-28T10:27:04.064Z" -http://www.frenchweb.fr/sanofi-sallie-a-google-pour-rebooster-sa-division-diabete/204734: - status: 200 - checked: "2025-03-28T10:27:05.983Z" -https://www.facebook.com/connectivity/: - status: 200 - checked: "2025-03-28T10:27:08.462Z" -https://fr.wikipedia.org/wiki/AlphaGo: - status: 200 - checked: "2025-03-28T10:27:09.603Z" -https://www.gv.com/portfolio/: - status: 200 - checked: "2025-03-28T10:27:11.605Z" -https://www.futura-sciences.com/tech/actualites/technologie-google-experimente-voitures-conducteur-25523/: - status: 200 - checked: "2025-03-28T10:27:12.630Z" -https://www.amazon.com/dp/8189059661: - status: 200 - checked: "2025-03-28T10:27:15.968Z" -https://fr.wikipedia.org/wiki/Révélations_de_télégrammes_de_la_diplomatie_américaine_par_WikiLeaks: - status: 200 - checked: "2025-03-28T10:27:18.886Z" -https://web.archive.org/web/20160802103953mp_/http://english.al-akhbar.com/content/stratforleaks-google-ideas-director-involved-regime-change: - status: 200 - checked: "2025-03-28T10:27:36.063Z" -https://web.archive.org/web/20160728123008mp_/http://europe.newsweek.com/assange-google-not-what-it-seems-279447?rm=eu: - status: 200 - checked: "2025-03-28T10:27:50.092Z" -https://web.archive.org/web/20160417095930mp_/http://www.france24.com/fr/20160218-apple-fbi-chine-iphone-securite-san-bernardino-censure-pekin-cook-posture: - status: 200 - checked: "2025-03-28T10:27:59.083Z" -https://fr.wikipedia.org/wiki/Attaque_de_l%27homme_du_milieu: - status: 200 - checked: "2025-03-28T10:28:00.171Z" -https://fr.wikipedia.org/wiki/Heuristique: - status: 200 - checked: "2025-03-28T10:28:00.870Z" -https://web.archive.org/web/20160811133128mp_/http://www.numerama.com/tech/188630-facebook-contourne-les-bloqueurs-de-publicite-pour-afficher-ses-pubs.html: - status: 200 - checked: "2025-03-28T10:28:17.497Z" -https://web.archive.org/web/20160811065905mp_/http://www.numerama.com/magazine/32094-google-microsoft-et-amazon-payent-adblock-plus-pour-un-laisser-passer.html: - status: 200 - checked: "2025-03-28T10:28:26.797Z" -https://web.archive.org/web/20160811065905mp_/https://fr.wikipedia.org/wiki/Adblock_Plus#Une_liste_de_filtres_blanche_activ.C3.A9e_par_d.C3.A9faut_depuis_2011: - status: 200 - checked: "2025-03-28T10:28:32.728Z" -https://web.archive.org/web/20160811065905mp_/http://www.nextinpact.com/news/100831-les-revenus-damazon-et-google-grimpent-notamment-grace-au-cloud.htm: - status: 200 - checked: "2025-03-28T10:28:46.575Z" -https://web.archive.org/web/20160811065905mp_/http://www.numerama.com/business/153796-adblockers-culpabiliser-linternaute-ne-sert-strictement-a-rien.html: - status: 200 - checked: "2025-03-28T10:28:57.309Z" -https://web.archive.org/web/20160811065905mp_/https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000000801164#LEGIARTI000018048180: - status: 200 - checked: "2025-03-28T10:29:10.965Z" -https://web.archive.org/web/20160811065905mp_/http://uk.businessinsider.com/facebook-q1-2016-earnings-2016-4?r=US&IR=T: - status: 200 - checked: "2025-03-28T10:29:22.726Z" -https://web.archive.org/web/20160811065905mp_/http://www.journaldunet.com/ebusiness/le-net/1125265-nombre-d-utilisateurs-de-facebook-dans-le-monde/: - status: 200 - checked: "2025-03-28T10:29:39.367Z" -https://web.archive.org/web/20160811065905mp_/http://www.numerama.com/business/185911-la-publicite-sur-les-fils-facebook-est-arrivee-a-saturation.html: - status: 200 - checked: "2025-03-28T10:29:48.796Z" -https://www.reddit.com/r/self/comments/f525iz/being_smarter_than_98_of_people_could_be_a/: - status: 200 - checked: "2025-03-28T10:29:50.189Z" -https://www.phpbb.com/: - status: 200 - checked: "2025-03-28T10:29:52.179Z" -https://linuxfr.org/: - status: 200 - checked: "2025-03-28T10:29:53.353Z" -https://fr.wikipedia.org/wiki/Booléen: - status: 200 - checked: "2025-03-28T10:29:54.646Z" -https://gohugo.io/: - status: 200 - checked: "2025-03-28T10:29:56.055Z" -https://wordpress.com: - status: 200 - checked: "2025-03-28T10:29:57.433Z" -https://fr.dotclear.org: - status: 200 - checked: "2025-03-28T10:29:59.218Z" -https://git.dern.ovh/richard/cyca: - status: 404 - checked: "2025-03-28T10:29:59.903Z" -http://www.gnu.org/licenses/gpl-3.0.html: - status: 200 - checked: "2025-03-28T10:30:01.142Z" -https://laravel.com: - status: 200 - checked: "2025-03-28T10:30:02.035Z" -https://vuejs.org/: - status: 200 - checked: "2025-03-28T10:30:02.950Z" -https://tailwindcss.com/: - status: 200 - checked: "2025-03-28T10:30:03.880Z" -https://fr.wikipedia.org/wiki/Homoglyphe: - status: 200 - checked: "2025-03-28T10:30:05.108Z" -https://docs.guzzlephp.org/en/stable/overview.html: - status: 200 - checked: "2025-03-28T10:30:06.040Z" -https://fr.wikipedia.org/wiki/Expression_régulière: - status: 200 - checked: "2025-03-28T10:30:07.465Z" -https://github.com/RichardDern/php-gemini: - status: 200 - checked: "2025-03-28T10:30:08.955Z" -https://gemini.circumlunar.space/: - status: null - checked: "2025-03-28T10:30:10.312Z" -https://global.download.synology.com/download/Document/Hardware/DataSheet/DiskStation/16-year/DS216play/enu/Synology_DS216play_Data_Sheet_enu.pdf: - status: 200 - checked: "2025-03-28T10:30:11.977Z" -https://www.st.com/resource/en/data_brief/stih412.pdf: - status: 200 - checked: "2025-03-28T10:30:13.310Z" -https://gitea.io/en-us/: - status: 200 - checked: "2025-03-28T10:30:14.352Z" -https://dl.gitea.io/gitea/1.13.1/gitea-1.13.1-linux-arm-6: - status: null - checked: "2025-03-28T10:30:15.535Z" -http://nas:13000/: - status: null - checked: "2025-03-28T10:30:16.130Z" -https://creativecommons.org/licenses/by-sa/4.0/deed.fr: - status: 200 - checked: "2025-03-28T10:30:17.015Z" -https://www.raspberrypi.com/news/raspberry-pi-os-64-bit/: - status: 200 - checked: "2025-03-28T10:30:18.191Z" -https://www.raspberrypi.org/interets/informatique/raspberry-pi-3-on-sale/: - status: 404 - checked: "2025-03-28T10:30:19.581Z" -https://en.wikipedia.org/wiki/ARM_architecture#64/32-bit_architecture: - status: 200 - checked: "2025-03-28T10:30:20.352Z" -https://www.debian.org/releases/buster/: - status: 200 - checked: "2025-03-28T10:30:21.193Z" -https://ubuntu.com/download/raspberry-pi: - status: 200 - checked: "2025-03-28T10:30:22.191Z" -https://alpinelinux.org/downloads/: - status: 200 - checked: "2025-03-28T10:30:22.926Z" -https://www.freebsd.org/where/: - status: 200 - checked: "2025-03-28T10:30:24.673Z" -https://bgr.com/2019/07/10/raspberry-pi-4-usb-c-charging-issue-how-to-fix-the-power-problem/: - status: 200 - checked: "2025-03-28T10:30:27.283Z" -https://matteocroce.medium.com/why-you-should-run-a-64-bit-os-on-your-raspberry-pi4-bd5290d48947: - status: 200 - checked: "2025-03-28T10:30:29.155Z" -https://www.wired.com/1997/01/did-gates-really-say-640k-is-enough-for-anyone/: - status: 200 - checked: "2025-03-28T10:30:30.601Z" -https://www.home-assistant.io/installation/raspberrypi: - status: 200 - checked: "2025-03-28T10:30:31.468Z" -https://en.wikipedia.org/wiki/Trim_(computing: - status: 404 - checked: "2025-03-28T10:30:32.650Z" -https://lemariva.com/interets/informatique/2020/08/raspberry-pi-4-ssd-booting-enabled-trim: - status: 404 - checked: "2025-03-28T10:30:34.178Z" -https://global.download.synology.com/download/Document/Hardware/DataSheet/Router/16-year/RT1900ac/fre/Synology_RT1900ac_Data_Sheet_fre.pdf: - status: 200 - checked: "2025-03-28T10:30:34.879Z" -https://www.synology.com/en-us/products/MR2200ac: - status: 200 - checked: "2025-03-28T10:30:37.759Z" -https://openwrt.org: - status: 200 - checked: "2025-03-28T10:30:39.211Z" -https://www.hardkernel.com: - status: 200 - checked: "2025-03-28T10:30:41.570Z" -http://www.banana-pi.org: - status: 200 - checked: "2025-03-28T10:30:44.355Z" -http://www.orangepi.org: - status: null - checked: "2025-03-28T10:30:46.181Z" -https://git.dern.ovh/Blog/contenu/src/branch/main/interets/informatique/2021/03/09/mon-reseau/index.md: - status: 404 - checked: "2025-03-28T10:30:46.891Z" -https://www.free.fr/freebox/freebox-pop/: - status: 200 - checked: "2025-03-28T10:30:47.858Z" -https://awowtech.com/products/awow-mini-pc-ak34: - status: 404 - checked: "2025-03-28T10:30:49.952Z" -https://opnsense.org/: - status: 200 - checked: "2025-03-28T10:30:50.973Z" -https://www.tp-link.com/fr/business-networking/unmanaged-switch/tl-sg1016/: - status: 200 - checked: "2025-03-28T10:30:52.596Z" -https://www.synology.com/fr-fr/products/MR2200ac: - status: 200 - checked: "2025-03-28T10:30:54.858Z" -https://www.home-assistant.io/: - status: 200 - checked: "2025-03-28T10:30:55.704Z" -https://www.espressif.com/en/products/socs/esp8266: - status: 200 - checked: "2025-03-28T10:30:56.619Z" -https://esphome.io/: - status: 200 - checked: "2025-03-28T10:30:57.559Z" -https://github.com/ccrisan/motioneye/: - status: 200 - checked: "2025-03-28T10:30:59.182Z" -https://www.amazon.fr/dp/B096R95YPG: - status: 200 - checked: "2025-03-28T10:31:02.051Z" -https://www.amazon.fr/dp/B0CJCBQYDY: - status: 200 - checked: "2025-03-28T10:31:06.037Z" -https://www.prestashop.com/fr: - status: 200 - checked: "2025-03-28T10:31:07.468Z" -https://github.com/PrestaShop/PrestaShop: - status: 200 - checked: "2025-03-28T10:31:08.957Z" -https://devdocs.prestashop.com/1.7/basics/installation/system-requirements/: - status: 200 - checked: "2025-03-28T10:31:09.975Z" -https://hub.docker.com/u/prestashop/#!: - status: 200 - checked: "2025-03-28T10:31:11.264Z" -https://magento.com: - status: 200 - checked: "2025-03-28T10:31:13.071Z" -https://github.com/magento/magento2: - status: 200 - checked: "2025-03-28T10:31:14.679Z" -https://devdocs.magento.com/guides/v2.4/install-gde/prereq/prereq-overview.html: - status: 200 - checked: "2025-03-28T10:31:15.888Z" -https://www.opencart.com: - status: 200 - checked: "2025-03-28T10:31:18.110Z" -https://github.com/opencart/opencart: - status: 200 - checked: "2025-03-28T10:31:19.959Z" -https://github.com/opencart/opencart/tree/master/upload/system/storage/vendor: - status: 200 - checked: "2025-03-28T10:31:21.420Z" -https://www.oscommerce.com/Us&News=177: - status: 200 - checked: "2025-03-28T10:31:25.109Z" -https://github.com/osCommerce: - status: 200 - checked: "2025-03-28T10:31:26.362Z" -https://opensource.org/licenses/OSL-3.0: - status: 200 - checked: "2025-03-28T10:31:27.496Z" -https://www.gnu.org/licenses/gpl-3.0.en.html: - status: 200 - checked: "2025-03-28T10:31:28.543Z" -https://fr.wikipedia.org/wiki/Alapage: - status: 200 - checked: "2025-03-28T10:31:29.634Z" -https://fr.finance.yahoo.com/quote/ORA.PA: - status: 200 - checked: "2025-03-28T10:31:32.059Z" -https://www.larecherche.fr/: - status: 200 - checked: "2025-03-28T10:31:33.025Z" -https://www.sciencesetavenir.fr: - status: 200 - checked: "2025-03-28T10:31:33.923Z" -https://www.apprendreaphilosopher.fr: - status: 200 - checked: "2025-03-28T10:31:35.149Z" -https://www.amazon.fr/dp/B07125HP8C: - status: 200 - checked: "2025-03-28T10:31:38.792Z" -https://git.dern.ovh/Livres: - status: 404 - checked: "2025-03-28T10:31:39.464Z" -https://w3techs.com/technologies/overview/content_management: - status: 200 - checked: "2025-03-28T10:31:40.740Z" -https://gohugo.io/content-management/formats/: - status: 200 - checked: "2025-03-28T10:31:41.500Z" -https://git.dern.ovh/: - status: 200 - checked: "2025-03-28T10:31:42.163Z" -https://git-scm.com: - status: 200 - checked: "2025-03-28T10:31:42.966Z" -https://github.com: - status: 200 - checked: "2025-03-28T10:31:44.150Z" -https://about.gitlab.com: - status: 200 - checked: "2025-03-28T10:31:45.973Z" -https://gohugo.io: - status: 200 - checked: "2025-03-28T10:31:46.757Z" -https://jekyllrb.com: - status: 200 - checked: "2025-03-28T10:31:47.525Z" -https://docs.drone.io/pipeline/overview/: - status: 200 - checked: "2025-03-28T10:31:48.975Z" -https://tailwindcss.com: - status: 200 - checked: "2025-03-28T10:31:49.684Z" -http://plugins.drone.io: - status: 200 - checked: "2025-03-28T10:31:51.637Z" -https://docs.drone.io/pipeline/triggers/: - status: 200 - checked: "2025-03-28T10:31:52.852Z" -https://www.drone.io: - status: 200 - checked: "2025-03-28T10:31:54.934Z" -https://ci.athaliasoft.com/login: - status: null - checked: "2025-03-28T10:31:55.637Z" -https://docs.drone.io/runner/overview/: - status: 200 - checked: "2025-03-28T10:31:56.947Z" -http://plugins.drone.io/: - status: 200 - checked: "2025-03-28T10:31:58.797Z" -https://duckduckgo.com/?q=prise+wattmetre&t=osx&iar=shopping&iax=shopping&ia=shopping: - status: 200 - checked: "2025-03-28T10:32:00.018Z" -https://fr.wikipedia.org/wiki/Étiquette-énergie: - status: 200 - checked: "2025-03-28T10:32:01.379Z" -https://www.numerama.com/tech/803201-mesurer-le-co2-selon-les-gigaoctets-consommes-lidee-qui-consterne-le-secteur-du-numerique.html: - status: 200 - checked: "2025-03-28T10:32:02.785Z" -https://www.forbes.com/sites/petersuciu/2021/04/16/do-we-need-to-worry-that-zoom-calls-use-too-much-energy/: - status: 200 - checked: "2025-03-28T10:32:04.837Z" -https://www.fsf.org/: - status: 200 - checked: "2025-03-28T10:32:08.357Z" -https://git-lfs.github.com: - status: 200 - checked: "2025-03-28T10:32:09.316Z" -https://syncthing.net/: - status: 200 - checked: "2025-03-28T10:32:10.557Z" -https://www.synology.com/en-us/dsm/feature/drive: - status: 200 - checked: "2025-03-28T10:32:13.624Z" -https://nextcloud.com/: - status: 200 - checked: "2025-03-28T10:32:15.011Z" -https://www.seafile.com/en/home/: - status: 200 - checked: "2025-03-28T10:32:16.840Z" -https://www.sparkleshare.org: - status: 200 - checked: "2025-03-28T10:32:17.722Z" -https://en.wikipedia.org/wiki/Overengineering: - status: 200 - checked: "2025-03-28T10:32:18.955Z" -https://github.com/git-lfs/git-lfs/wiki/FAQ: - status: 200 - checked: "2025-03-28T10:32:20.124Z" -https://github.com/git-lfs/git-lfs/wiki: - status: 200 - checked: "2025-03-28T10:32:21.305Z" -https://archive.org/web/: - status: 200 - checked: "2025-03-28T10:32:24.044Z" -https://en.wikipedia.org/wiki/Calico_(company: - status: 404 - checked: "2025-03-28T10:32:25.484Z" -https://web.archive.org/: - status: 200 - checked: "2025-03-28T10:32:27.796Z" -https://web.archive.org/web/20080405051026/http://groups.msn.com/LeparadisInformatique/chronologieduparadis.msnw: - status: 200 - checked: "2025-03-28T10:32:37.060Z" -http://10.0.2.1:9000: - status: null - checked: "2025-03-28T10:32:38.616Z" -https://caddyserver.com/docs/caddyfile/matchers#syntax: - status: 200 - checked: "2025-03-28T10:32:40.328Z" -https://caddyserver.com/docs/caddyfile/matchers#path-regexp: - status: 200 - checked: "2025-03-28T10:32:41.973Z" -https://github.com/wjdp/htmltest: - status: 200 - checked: "2025-03-28T10:32:43.575Z" -https://github.com/wjdp/htmltest#wrench-configuration: - status: 200 - checked: "2025-03-28T10:32:45.236Z" -https://nixos.org/: - status: 200 - checked: "2025-03-28T10:32:46.088Z" -https://fr.wikipedia.org/wiki/NixOS: - status: 200 - checked: "2025-03-28T10:32:47.822Z" -https://steamcommunity.com/id/richarddern/games/?tab=all&sort=playtime: - status: 200 - checked: "2025-03-28T10:32:48.951Z" -https://www.lesnumeriques.com/souris/molettes-instables-resultats-sondage-reponse-logitech-n60523.html: - status: 200 - checked: "2025-03-28T10:32:50.718Z" -https://github.com/flozz/rivalcfg: - status: 200 - checked: "2025-03-28T10:32:52.146Z" -https://github.com/flozz/rivalcfg/issues/146: - status: 200 - checked: "2025-03-28T10:32:53.960Z" -https://www.asus.com/fr/Commercial-Laptops/ASUS_Transformer_Book_T100TA/: - status: 200 - checked: "2025-03-28T10:33:01.463Z" -https://qutebrowser.org/: - status: 200 - checked: "2025-03-28T10:33:02.574Z" -https://channels.nixos.org/nixos-21.05/latest-nixos-minimal-x86_64-linux.iso: - status: null - checked: "2025-03-28T10:33:03.735Z" -https://channels.nixos.org/nixos-21.05/latest-nixos-minimal-i686-linux.iso: - status: null - checked: "2025-03-28T10:33:04.757Z" -https://nixos.org/manual/nixos/stable/: - status: 200 - checked: "2025-03-28T10:33:05.922Z" -https://www.journalduhacker.net/: - status: 200 - checked: "2025-03-28T10:33:06.900Z" -https://www.journalduhacker.net/s/rsveeh/mon_r_seau: - status: 200 - checked: "2025-03-28T10:33:07.784Z" -https://www.journalduhacker.net/s/ijrnnq/e_commerce_et_auto_h_bergement: - status: 200 - checked: "2025-03-28T10:33:08.588Z" -https://www.journalduhacker.net/s/cnmgcu/deux_semaines_sous_nixos_je_divorce_de: - status: 200 - checked: "2025-03-28T10:33:09.235Z" -https://www.journalduhacker.net/s/gvlelh/l_co_responsabilit_en_informatique: - status: 200 - checked: "2025-03-28T10:33:09.882Z" -https://fr.wikipedia.org/wiki/Fediverse: - status: 200 - checked: "2025-03-28T10:33:11.229Z" -https://pleroma.social/: - status: 200 - checked: "2025-03-28T10:33:12.346Z" -https://github.com/misskey-dev/misskey: - status: 200 - checked: "2025-03-28T10:33:13.835Z" -https://mastodon.social/about: - status: 200 - checked: "2025-03-28T10:33:15.323Z" -https://matrix.org/: - status: 200 - checked: "2025-03-28T10:33:16.430Z" -https://books.apple.com/fr/book/until-the-end-of-time/id1478202295: - status: 200 - checked: "2025-03-28T10:33:18.349Z" -https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewBook?id=1165518487: - status: null - checked: "2025-03-28T10:33:19.104Z" -https://www.amazon.fr/dp/B07CVJ9WFC: - status: 200 - checked: "2025-03-28T10:33:21.691Z" -https://www.amazon.fr/dp/B07JR9DKWW: - status: 200 - checked: "2025-03-28T10:33:24.538Z" -https://steamcommunity.com/app/427520: - status: 200 - checked: "2025-03-28T10:33:28.327Z" -https://store.steampowered.com/app/211820: - status: 200 - checked: "2025-03-28T10:33:29.620Z" -https://store.steampowered.com/app/24720: - status: 200 - checked: "2025-03-28T10:33:31.243Z" -https://store.steampowered.com/app/1213210: - status: 200 - checked: "2025-03-28T10:33:32.366Z" -https://fr.wikipedia.org/wiki/Le_Guide_du_voyageur_galactique: - status: 200 - checked: "2025-03-28T10:33:33.283Z" -https://www.schleich-s.com/fr/FR/dinosaurs/produits/mosasaurus-15026.html: - status: 200 - checked: "2025-03-28T10:33:35.681Z" -https://www.schleich-s.com/fr/FR/dinosaurs/produits/carnotaurus-14586.html: - status: 200 - checked: "2025-03-28T10:33:37.526Z" -https://fr.wikipedia.org/wiki/Peste_noire: - status: 200 - checked: "2025-03-28T10:33:38.316Z" -https://fr.wikipedia.org/wiki/Grippe_espagnole: - status: 200 - checked: "2025-03-28T10:33:39.062Z" -https://fr.wikipedia.org/wiki/Grippe_asiatique: - status: 200 - checked: "2025-03-28T10:33:40.655Z" -https://fr.wikipedia.org/wiki/Grippe_de_Hong_Kong: - status: 200 - checked: "2025-03-28T10:33:41.948Z" -https://tech-fairy.com/keyboards-form-factors-explained-full-size-100-1800-compact-96-tenkeyless-tkl-87-80-1800-compact-75-60-40-tenkey-5/: - status: 200 - checked: "2025-03-28T10:33:42.851Z" -https://ergodox-ez.com: - status: 200 - checked: "2025-03-28T10:33:43.922Z" -https://www.amazon.fr/dp/B08BC4GD6G: - status: 200 - checked: "2025-03-28T10:33:47.831Z" -https://www.keychron.com/products/keychron-k12-wireless-mechanical-keyboard?variant=39299048276057: - status: 200 - checked: "2025-03-28T10:33:50.692Z" -https://www.keychron.com/products/keychron-q2-qmk-custom-mechanical-keyboard?variant=39610695680089: - status: 200 - checked: "2025-03-28T10:33:53.068Z" -https://qmk.fm: - status: 200 - checked: "2025-03-28T10:33:54.156Z" -https://caniusevia.com: - status: 200 - checked: "2025-03-28T10:33:55.004Z" -https://www.pcgamingrace.com/products/the-glorious-gmmk-compact-pre-built: - status: 200 - checked: "2025-03-28T10:33:58.504Z" -https://www.kmovetech.com/kemove-61-key-white-p0023-p0075.html: - status: null - checked: "2025-03-28T10:34:00.795Z" -https://ludwigdn.dev: - status: 200 - checked: "2025-03-28T10:34:01.807Z" -https://www.duckychannel.com.tw/en/One-2-Mini-Skyline: - status: 200 - checked: "2025-03-28T10:34:05.045Z" -https://keygem.store/collections/tools/products/kbdfans-switch-lube-station: - status: 404 - checked: "2025-03-28T10:34:07.303Z" -https://www.amazon.fr/dp/B008DHZXRC: - status: 200 - checked: "2025-03-28T10:34:11.024Z" -https://kbdfans.com/products/kbdfans-switches-pads-2-versions?pr_prod_strat=copurchase&pr_rec_pid=6605988561035&pr_ref_pid=5032495317131&pr_seq=uniform: - status: 200 - checked: "2025-03-28T10:34:12.608Z" -https://keygem.store/collections/accessories/products/kbdfans-stabilizers-foam-sticker-20pcs: - status: 200 - checked: "2025-03-28T10:34:14.266Z" -https://kbdfans.com/products/kbdfans-switch-films?variant=34314588815499: - status: 200 - checked: "2025-03-28T10:34:15.474Z" -https://keygem.store/collections/springs: - status: 200 - checked: "2025-03-28T10:34:17.065Z" -https://keygem.store/collections/accessories/products/switches-kailh-pcb-socket-10pcs: - status: 200 - checked: "2025-03-28T10:34:18.706Z" -https://keygem.store/collections/accessories/products/kbdfans-mechanical-keyboard-spacebar-foam: - status: 200 - checked: "2025-03-28T10:34:20.388Z" -https://keygem.store/products/m2-washers?_pos=1&_sid=8f529e6f4&_ss=r: - status: 200 - checked: "2025-03-28T10:34:21.975Z" -https://kbdfans.com/products/kbdfans-standoff-silicone-cover: - status: 200 - checked: "2025-03-28T10:34:23.144Z" -https://keygem.store/products/replacement-feet-bumpers-4pcs?_pos=1&_sid=2b0f82876&_ss=r: - status: 200 - checked: "2025-03-28T10:34:24.771Z" -https://mykeyboard.eu/catalogue/crin-extra-custom-feet_5417/: - status: null - checked: "2025-03-28T10:34:25.634Z" -https://kbdfans.com/products/dz60rgb-ansi-pcb-foam: - status: 404 - checked: "2025-03-28T10:34:26.490Z" -https://kbdfans.com/collections/60/products/dz60rgb-ansi-mechanical-keyboard-pcb: - status: 404 - checked: "2025-03-28T10:34:27.356Z" -https://ben.rethore.im: - status: 200 - checked: "2025-03-28T10:34:29.187Z" -https://caniusevia.com/: - status: 200 - checked: "2025-03-28T10:34:29.882Z" -https://kbdfans.com/collections/keyboard-stabilizer/products/gmk-screw-in-stabilizers?variant=22154915348528: - status: 404 - checked: "2025-03-28T10:34:30.913Z" -https://www.youtube.com/watch?v=-vhpHjlkRgQ: - status: null - checked: "2025-03-28T10:34:31.457Z" -https://drop.com/buy/drop-skylight-series-keycap-set?referer=KPNLR3: - status: null - checked: "2025-03-28T10:34:31.992Z" -https://kbdfans.com/collections/wrist-rest/products/handmade-resin-wrist-rest-1?variant=39444177223819: - status: null - checked: "2025-03-28T10:34:32.535Z" -https://cntronic.com/emk-90-degree-usb-type-c-cable-usb-c-cable-type-c-fast-charging-cord-for-nintendo-switch-samsung-s8-oneplus-5-pixel-2-0-3m-1-2m-28446: - status: null - checked: "2025-03-28T10:34:33.084Z" -http://localhost:1313/interets/informatique/2022/01/11/a-la-recherche-du-clavier-parfait-un-clavier-100-custom/: - status: 200 - checked: "2025-03-28T10:34:33.709Z" -https://www.pcgamingrace.com/products/glorious-gmmk-pro-75-barebone-black: - status: 200 - checked: "2025-03-28T10:34:36.185Z" -https://www.keychron.com/products/keychron-q2-qmk-custom-mechanical-keyboard: - status: 200 - checked: "2025-03-28T10:34:38.617Z" -https://www.duckychannel.com.tw/en/Keyboards/One3-Series: - status: 200 - checked: "2025-03-28T10:34:40.142Z" -https://www.keychron.com/products/keychron-q2-qmk-custom-mechanical-keyboard?variant=39610696400985: - status: 200 - checked: "2025-03-28T10:34:42.848Z" -https://mechanicalkeyboards.com/shop/index.php?p=5449&l=product_detail&my_rate=USD: - status: 200 - checked: "2025-03-28T10:34:45.150Z" -https://matrix.to/#/@zoz:matrix.zoz-serv.org: - status: 200 - checked: "2025-03-28T10:34:46.178Z" -https://fr.wikipedia.org/wiki/Epoch: - status: 200 - checked: "2025-03-28T10:34:47.551Z" -https://fr.wikipedia.org/wiki/Tim_Berners-Lee: - status: 200 - checked: "2025-03-28T10:34:48.279Z" -https://home.cern/fr/science/computing/birth-web: - status: 200 - checked: "2025-03-28T10:34:50.397Z" -https://fr.wikipedia.org/wiki/Déclaration_d: - status: 404 - checked: "2025-03-28T10:34:51.526Z" -https://fr.wikipedia.org/wiki/Nombre_de_Dunbar: - status: 200 - checked: "2025-03-28T10:34:52.227Z" -https://www.w3.org: - status: 200 - checked: "2025-03-28T10:34:53.115Z" -https://www.koreatimes.co.kr/www/tech/2021/10/129_308975.html: - status: 200 - checked: "2025-03-28T10:34:59.614Z" -https://www.meta-media.fr/2021/12/10/coree-du-sud-le-metavers-pour-tous.html: - status: 200 - checked: "2025-03-28T10:35:01.333Z" -https://cryptoast.fr/achat-biens-immobiliers-metaverse-investissement-judicieux/: - status: 200 - checked: "2025-03-28T10:35:13.337Z" -https://fr.wikipedia.org/wiki/Blockchain: - status: 200 - checked: "2025-03-28T10:35:14.120Z" -https://fr.wikipedia.org/wiki/Berkeley_Open_Infrastructure_for_Network_Computing: - status: 200 - checked: "2025-03-28T10:35:16.166Z" -https://fr.wikipedia.org/wiki/Bitcoin#Principe_monétaire: - status: 200 - checked: "2025-03-28T10:35:17.039Z" -https://www.cnbc.com/2021/11/30/looking-for-a-job-you-might-get-hired-via-the-metaverse-experts-say.html: - status: 200 - checked: "2025-03-28T10:35:19.589Z" -https://www.indeed.fr/: - status: 403 - checked: "2025-03-28T10:35:20.406Z" -https://fr.wikipedia.org/wiki/Catastrophe_malthusienne: - status: 200 - checked: "2025-03-28T10:35:21.630Z" -https://en.akkogear.com/product/mod-007-aluminum-diy-kit/: - status: 200 - checked: "2025-03-28T10:35:24.038Z" -https://www.eloquentclicks.com/: - status: 200 - checked: "2025-03-28T10:35:25.908Z" -http://mechanicalkeyboards.com/: - status: 200 - checked: "2025-03-28T10:35:28.075Z" -https://shop.tai-hao.com/products/pbt-backlit-c12bl201: - status: 200 - checked: "2025-03-28T10:35:32.396Z" -https://mechanicalkeyboards.com: - status: 200 - checked: "2025-03-28T10:35:33.963Z" -https://www.amazon.fr/dp/B07885QL77: - status: 200 - checked: "2025-03-28T10:35:38.021Z" -https://www.amazon.fr/dp/B099DY17K9: - status: 200 - checked: "2025-03-28T10:35:40.683Z" -https://www.kailhswitch.com/mechanical-keyboard-switches/smt-key-switches/box-blue-white-switches-for-mechanical.html: - status: 200 - checked: "2025-03-28T10:35:44.509Z" -https://shop.tai-hao.com/: - status: 200 - checked: "2025-03-28T10:35:47.637Z" -https://en.akkogear.com/download/: - status: 200 - checked: "2025-03-28T10:35:49.343Z" -https://qmk.fm/: - status: 200 - checked: "2025-03-28T10:35:50.079Z" -https://mechanicalkeyboards.com/: - status: 200 - checked: "2025-03-28T10:35:51.342Z" -https://airtable.com/shr2beqjUXzTk6GfS/tblYLigKdFcOriJ1k?backgroundColor=orange&viewControls=on: - status: 200 - checked: "2025-03-28T10:35:54.844Z" -https://www.ldlc.com/: - status: 200 - checked: "2025-03-28T10:35:56.531Z" -https://gohugo.io/functions/time/format/: - status: 200 - checked: "2025-03-28T10:35:57.275Z" -https://pkg.go.dev/time#ANSIC: - status: 200 - checked: "2025-03-28T10:35:58.631Z" -https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-time-element: - status: 200 - checked: "2025-03-28T10:36:00.163Z" -https://gohugo.io/templates/internal/: - status: 200 - checked: "2025-03-28T10:36:01.115Z" -https://laravel.com/: - status: 200 - checked: "2025-03-28T10:36:01.896Z" -https://tailwindcss.com/docs/functions-and-directives#apply: - status: 200 - checked: "2025-03-28T10:36:02.572Z" -https://tailwindcss.com/docs/reusing-styles#extracting-components-and-partials: - status: 200 - checked: "2025-03-28T10:36:03.528Z" -https://jigsaw.w3.org/css-validator/about.html: - status: 400 - checked: "2025-03-28T10:36:04.300Z" -https://tailwindcss.com/docs/utility-first: - status: 200 - checked: "2025-03-28T10:36:05.035Z" -https://jigsaw.tighten.co: - status: 200 - checked: "2025-03-28T10:36:06.453Z" -https://www.wemos.cc/en/latest/d1/d1_mini.html: - status: 200 - checked: "2025-03-28T10:36:07.398Z" -https://fr.wikipedia.org/wiki/I2C: - status: 200 - checked: "2025-03-28T10:36:08.140Z" -https://sensirion.com/products/catalog/SHT31-DIS-B/: - status: 200 - checked: "2025-03-28T10:36:10.250Z" -https://ams.com/en/TSL2561: - status: 200 - checked: "2025-03-28T10:36:11.931Z" -https://www.amazon.fr/dp/B07FJZQLMK/: - status: 200 - checked: "2025-03-28T10:36:14.765Z" -https://www.amazon.fr/dp/B07HSZ64L2: - status: 200 - checked: "2025-03-28T10:36:18.509Z" -https://www.bosch-sensortec.com/products/environmental-sensors/humidity-sensors-bme280/: - status: 200 - checked: "2025-03-28T10:36:20.551Z" -http://www.embeddedadventures.com/as3935_lightning_sensor_module_mod-1016.html: - status: 200 - checked: "2025-03-28T10:36:21.935Z" -https://meteofrance.com/: - status: 200 - checked: "2025-03-28T10:36:23.233Z" -https://www.home-assistant.io: - status: 200 - checked: "2025-03-28T10:36:24.034Z" -https://en.wikipedia.org/wiki/Serial_Peripheral_Interface: - status: 200 - checked: "2025-03-28T10:36:24.799Z" -https://en.wikipedia.org/wiki/1-Wire: - status: 200 - checked: "2025-03-28T10:36:25.522Z" -https://www.sudouest.fr/gironde/mios/bassin-d-arcachon-le-souffle-du-volcan-du-tonga-capte-jusque-dans-les-stations-meteo-de-mios-7801891.php: - status: 200 - checked: "2025-03-28T10:36:27.134Z" -https://fr.wikipedia.org/wiki/Anémomètre: - status: 200 - checked: "2025-03-28T10:36:29.531Z" -https://fr.wikipedia.org/wiki/Pluviomètre: - status: 200 - checked: "2025-03-28T10:36:30.776Z" -https://fr.wikipedia.org/wiki/Girouette: - status: 200 - checked: "2025-03-28T10:36:32.097Z" -https://fr.wikipedia.org/wiki/Magnétomètre: - status: 200 - checked: "2025-03-28T10:36:33.351Z" -https://fr.wikipedia.org/wiki/Boussole_électronique: - status: 200 - checked: "2025-03-28T10:36:34.482Z" -http://wiki.sunfounder.cc/index.php?title=QMC5883L: - status: null - checked: "2025-03-28T10:36:35.744Z" -https://fr.wikipedia.org/wiki/Compteur_Geiger: - status: 200 - checked: "2025-03-28T10:36:36.454Z" -https://mightyohm.com/interets/informatique/products/geiger-counter/: - status: 404 - checked: "2025-03-28T10:36:37.702Z" -http://www.ntp.org: - status: 200 - checked: "2025-03-28T10:36:38.939Z" -https://www.espressif.com/en/products/socs/esp32: - status: 200 - checked: "2025-03-28T10:36:39.754Z" -https://fr.wiktionary.org/wiki/procrastination: - status: 200 - checked: "2025-03-28T10:36:41.176Z" -https://fr.wikipedia.org/wiki/Oblomovisme: - status: 200 - checked: "2025-03-28T10:36:42.352Z" -https://en.akkogear.com/product/akko-cs-rose-red-switch/: - status: 200 - checked: "2025-03-28T10:36:44.083Z" -https://en.akkogear.com/product/macaw-keycap-set-157-key/: - status: 200 - checked: "2025-03-28T10:36:45.849Z" -https://en.akkogear.com/product/aviator-coiled-cable/: - status: 200 - checked: "2025-03-28T10:36:47.567Z" -https://steelseries.com/gaming-mice/rival-3-wireless: - status: 200 - checked: "2025-03-28T10:36:53.654Z" -https://www.lexip.co/fr/produits/Mo42: - status: 200 - checked: "2025-03-28T10:36:57.421Z" -http://www.kailh.com/en/Products/Ks/BOXS/315.html: - status: 404 - checked: "2025-03-28T10:37:00.726Z" -https://www.amazon.fr/dp/B07VV7MK54: - status: 200 - checked: "2025-03-28T10:37:04.393Z" -https://www.intel.com/content/www/us/en/products/sku/97129/intel-core-i77700k-processor-8m-cache-up-to-4-50-ghz/specifications.html: - status: 200 - checked: "2025-03-28T10:37:06.629Z" -https://rog.asus.com/motherboards/rog-strix/rog-strix-z270f-gaming-model/: - status: 200 - checked: "2025-03-28T10:37:08.263Z" -https://www.msi.com/Graphics-Card/GeForce-GTX-1070-ARMOR-8G-OC: - status: 200 - checked: "2025-03-28T10:37:10.923Z" -https://www.intel.fr/content/www/fr/fr/products/sku/134594/intel-core-i712700k-processor-25m-cache-up-to-5-00-ghz/specifications.html: - status: 200 - checked: "2025-03-28T10:37:12.453Z" -https://fr.msi.com/Liquid-Cooling/MAG-CORELIQUID-C240: - status: 200 - checked: "2025-03-28T10:37:13.857Z" -https://www.asrock.com/mb/Intel/B660M_Steel_Legend/index.asp: - status: 404 - checked: "2025-03-28T10:37:14.767Z" -https://www.asrock.com/mb/AMD/E350M1/: - status: 200 - checked: "2025-03-28T10:37:15.601Z" -https://www.amd.com/fr/products/processors/desktops/ryzen/5000-series/amd-ryzen-9-5950x.html: - status: 200 - checked: "2025-03-28T10:37:17.887Z" -https://www.asrock.com/mb/AMD/B550M_Pro4/index.asp: - status: 404 - checked: "2025-03-28T10:37:18.672Z" -https://www.coolermaster.com/catalog/legacy-products/cooling/hyper-412s/: - status: 200 - checked: "2025-03-28T10:37:21.353Z" -https://www.materiel.net/: - status: 200 - checked: "2025-03-28T10:37:22.298Z" -https://noctua.at/en/nm-i17xx-mp78-mounting-kit: - status: 200 - checked: "2025-03-28T10:37:23.924Z" -https://www.amazon.fr/dp/B09FSTZM2G: - status: 200 - checked: "2025-03-28T10:37:27.269Z" -https://pop.system76.com/: - status: 200 - checked: "2025-03-28T10:37:29.046Z" -http://nextcloud.com: - status: 200 - checked: "2025-03-28T10:37:30.105Z" -https://fr.wikipedia.org/wiki/Fichier_et_répertoire_caché: - status: 200 - checked: "2025-03-28T10:37:31.257Z" -https://gitea.io/: - status: 200 - checked: "2025-03-28T10:37:32.092Z" -https://git-lfs.github.com/: - status: 200 - checked: "2025-03-28T10:37:32.795Z" -https://nixos.org/manual/nixos/stable/index.html#sec-building-image: - status: 200 - checked: "2025-03-28T10:37:34.047Z" -https://min.io/: - status: 200 - checked: "2025-03-28T10:37:36.807Z" -http://www.gluster.org/: - status: 200 - checked: "2025-03-28T10:37:38.272Z" -https://ceph.com/: - status: 200 - checked: "2025-03-28T10:37:39.656Z" -https://ffmpeg.org: - status: 200 - checked: "2025-03-28T10:37:40.657Z" -https://www.collaboraoffice.com/fr/collabora-online/: - status: 200 - checked: "2025-03-28T10:37:44.037Z" -https://about.gitlab.com/fr-fr/: - status: 200 - checked: "2025-03-28T10:37:45.573Z" -https://github.com/: - status: 200 - checked: "2025-03-28T10:37:46.573Z" -https://pages.github.com/: - status: 200 - checked: "2025-03-28T10:37:47.276Z" -https://gist.github.com: - status: 200 - checked: "2025-03-28T10:37:48.671Z" -https://hedgedoc.org: - status: 200 - checked: "2025-03-28T10:37:49.770Z" -https://duckduckgo.com/?q=alienware+aurora+ryzen+r10&t=osx&iax=images&iai=https%3A%2F%2Fhameg.fr%2Fwp-content%2Fuploads%2F2020%2F11%2FTest-de-lAlienware-Aurora-R10-Edition-Ryzen-3-1024x1024.jpg&ia=images: - status: 200 - checked: "2025-03-28T10:37:50.893Z" -https://pocketnow.com/explaining-windows-11s-bad-design: - status: null - checked: "2025-03-28T10:37:51.515Z" -https://www.pcworld.com/article/539089/why-we-recommend-you-skip-windows-11-for-now.html: - status: 200 - checked: "2025-03-28T10:37:54.693Z" -https://github.com/builtbybel/ThisIsWin11: - status: 200 - checked: "2025-03-28T10:37:56.512Z" -https://www.nobelprize.org/prizes/peace/1999/summary/: - status: 200 - checked: "2025-03-28T10:37:57.815Z" -https://fr.wikipedia.org/wiki/Jacques_Crozemarie: - status: 200 - checked: "2025-03-28T10:37:58.555Z" -https://www.amazon.fr/dp/B09W252VMT: - status: 200 - checked: "2025-03-28T10:38:01.522Z" -https://epomaker.com: - status: 200 - checked: "2025-03-28T10:38:05.343Z" -https://divinikey.com/products/durock-v2-stabilizers-screw-in: - status: 200 - checked: "2025-03-28T10:38:07.150Z" -http://gadzikowski.com/nkeyrollover.html: - status: null - checked: "2025-03-28T10:38:08.257Z" -https://fr.steelseries.com/: - status: 200 - checked: "2025-03-28T10:38:10.513Z" -https://fr.steelseries.com/gaming-mice/aerox-3-2022?cable-color=black&cable-length=long&cable-material=super-mesh&cable-usb-type=a-to-c&connectivity=wired&mouse-body-color=ghost&mouse-feet-color=black&mouse-feet-material=ptfe: - status: 200 - checked: "2025-03-28T10:38:15.233Z" -https://fr.steelseries.com/gaming-accessories/replacement-parts: - status: 200 - checked: "2025-03-28T10:38:19.741Z" -https://www.icloud.com/sharedalbum/fr-fr/#B0rGI9HKKu20DY7: - status: 200 - checked: "2025-03-28T10:38:21.004Z" -https://www.sciencesetavenir.fr/high-tech/intelligence-artificielle/l-ia-de-microsoft-est-elle-reellement-devenue-raciste-au-contact-des-internautes_31260: - status: 200 - checked: "2025-03-28T10:38:22.529Z" -https://web.archive.org/web/20200730111553/https://www.reuters.com/article/us-amazon-com-jobs-automation-insight/amazon-scraps-secret-ai-recruiting-tool-that-showed-bias-against-women-idUSKCN1MK08G: - status: 200 - checked: "2025-03-28T10:38:40.340Z" -https://en.wikipedia.org/wiki/Stable_Diffusion: - status: 200 - checked: "2025-03-28T10:38:41.173Z" -https://diffusionbee.com: - status: 200 - checked: "2025-03-28T10:38:42.646Z" -https://www.ea.com/fr-fr/games/spore: - status: 200 - checked: "2025-03-28T10:38:44.831Z" -https://matrix.to/#/@olivier:matrix.athaliasoft.com: - status: 200 - checked: "2025-03-28T10:38:45.573Z" -https://nixos.org/manual/nixos/stable: - status: 200 - checked: "2025-03-28T10:38:46.743Z" -https://store.steampowered.com/: - status: 200 - checked: "2025-03-28T10:38:47.922Z" -https://i3wm.org/: - status: 200 - checked: "2025-03-28T10:38:48.697Z" -https://xfce.org/: - status: 200 - checked: "2025-03-28T10:38:49.566Z" -https://www.tomshardware.com/reviews/nvidia-geforce-rtx-3080-review/4: - status: 200 - checked: "2025-03-28T10:38:50.684Z" -https://www.amazon.fr/dp/B08ZYX5ZFV: - status: 200 - checked: "2025-03-28T10:38:53.671Z" -https://nixos.org/manual/nix/stable/: - status: 200 - checked: "2025-03-28T10:38:54.533Z" -https://www.lenovo.com/fr/fr/tablets/android-tablets/lenovo-tab-series/Tab-M10/p/ZZITZTATBBX: - status: 200 - checked: "2025-03-28T10:38:59.741Z" -https://devices.ubuntu-touch.io/device/x605/: - status: 200 - checked: "2025-03-28T10:39:00.649Z" -https://www.linuxfromscratch.org/: - status: 200 - checked: "2025-03-28T10:39:02.322Z" -https://nixos.wiki/wiki/Flakes: - status: 200 - checked: "2025-03-28T10:39:03.321Z" -https://vitejs.dev: - status: 200 - checked: "2025-03-28T10:39:05.517Z" -https://vuejs.org: - status: 200 - checked: "2025-03-28T10:39:06.282Z" -https://vuetifyjs.com/en/: - status: 200 - checked: "2025-03-28T10:39:07.441Z" -https://next.vuetifyjs.com/en/getting-started/installation/#manual-steps: - status: 200 - checked: "2025-03-28T10:39:08.375Z" -https://next.vuetifyjs.com/en/getting-started/installation/#installation: - status: 200 - checked: "2025-03-28T10:39:09.269Z" -https://fr.wikipedia.org/wiki/Paradoxe_de_Jevons: - status: 200 - checked: "2025-03-28T10:39:10.481Z" -https://next.vuetifyjs.com/en/styles/css-reset/: - status: 200 - checked: "2025-03-28T10:39:11.391Z" -https://www.sciencesetavenir.fr/: - status: 200 - checked: "2025-03-28T10:39:12.223Z" -https://www.pourlascience.fr: - status: 200 - checked: "2025-03-28T10:39:14.572Z" -https://en.wikipedia.org/wiki/The_Dinosaur_Heresies: - status: 200 - checked: "2025-03-28T10:39:15.441Z" -https://especes.org/: - status: 200 - checked: "2025-03-28T10:39:16.731Z" -https://especes.org/produit/la-paleontologie-a-200-ans-n-45/: - status: 404 - checked: "2025-03-28T10:39:19.339Z" -https://fr.duolingo.com/: - status: 200 - checked: "2025-03-28T10:39:20.966Z" -https://www.dell.com/fr-fr/shop/écran-de-gaming-alienware-25-aw2521h/apd/210-aycl/moniteurs-et-accessoires-de-moniteur: - status: 200 - checked: "2025-03-28T10:39:22.599Z" -https://fr.wikipedia.org/wiki/Maladie_à_coronavirus_2019#Traitement: - status: 200 - checked: "2025-03-28T10:39:38.157Z" -https://www.nasa.gov/webbfirstimages: - status: 200 - checked: "2025-03-28T10:39:39.292Z" -https://tech.co/news/data-breaches-2022-so-far: - status: 200 - checked: "2025-03-28T10:39:40.561Z" -https://www.20minutes.fr/high-tech/4015463-20221219-twitter-elon-musk-lance-sondage-savoir-doit-non-demissionner: - status: 200 - checked: "2025-03-28T10:39:42.136Z" -https://www.larousse.fr/dictionnaires/francais/identité/41420: - status: 200 - checked: "2025-03-28T10:39:43.423Z" -https://www.sciencesetavenir.fr/high-tech/intelligence-artificielle/un-tableau-peint-par-une-ia-vendu-a-plus-de-400-000-dollars_128993: - status: 200 - checked: "2025-03-28T10:39:44.540Z" -https://fr.wikipedia.org/wiki/Deepfake: - status: 200 - checked: "2025-03-28T10:39:45.280Z" -https://www.intel.com/content/www/us/en/newsroom/news/intel-introduces-real-time-deepfake-detector.html: - status: 200 - checked: "2025-03-28T10:39:47.352Z" -https://fr.wikipedia.org/wiki/Test_de_Turing: - status: 200 - checked: "2025-03-28T10:39:48.080Z" -https://en.wikipedia.org/wiki/Superfish: - status: 200 - checked: "2025-03-28T10:39:48.783Z" -https://linuxfr.org/users/richarddern/journaux/a-propos-des-certificats: - status: 200 - checked: "2025-03-28T10:39:50.056Z" -https://fr.wiktionary.org/wiki/sémiochimie: - status: 200 - checked: "2025-03-28T10:39:51.215Z" -https://www.oxia-palus.com/: - status: 200 - checked: "2025-03-28T10:39:52.808Z" -https://www.sciencesetavenir.fr/high-tech/intelligence-artificielle/la-10e-symphonie-de-beethoven-une-oeuvre-de-l-apprentissage-automatique_157486: - status: 200 - checked: "2025-03-28T10:39:54.050Z" -https://www.numerama.com/tech/289831-robot-journaliste-en-un-an-une-ia-creee-par-le-washington-post-a-publie-850-articles.html: - status: 200 - checked: "2025-03-28T10:39:55.486Z" -https://www.infoclimat.fr/: - status: 200 - checked: "2025-03-28T10:39:56.692Z" -https://asso.infoclimat.fr/: - status: 200 - checked: "2025-03-28T10:39:57.541Z" -https://asso.infoclimat.fr/adherents/assemblees-generales.php: - status: 200 - checked: "2025-03-28T10:39:58.398Z" -https://www.infoclimat.fr: - status: 200 - checked: "2025-03-28T10:39:59.469Z" -https://www.infoclimat.fr/apprendre-lexique-meteo.html: - status: 200 - checked: "2025-03-28T10:40:00.386Z" -https://forums.infoclimat.fr/: - status: 200 - checked: "2025-03-28T10:40:01.337Z" -https://asso.infoclimat.fr/adherents/pdf/PV-AGE-04-2022.pdf: - status: 200 - checked: "2025-03-28T10:40:02.010Z" -https://www.homecine-compare.com/lecteur-SONBDPS490-SONY-BDP-S490.htm: - status: 200 - checked: "2025-03-28T10:40:04.179Z" -https://www.homecinesolutions.fr/p/17808-panasonic-dp-ub820efk?utm_campaign=&utm_content=&utm_source=Bing+Ads&utm_medium=cpc&utm_term=Panasonic+DP-UB820EFK&msclkid=fbb47cc09bf0167b6d081f7686db3f98: - status: 200 - checked: "2025-03-28T10:40:05.741Z" -https://www.adafruit.com/: - status: 200 - checked: "2025-03-28T10:40:07.072Z" -https://learn.adafruit.com/introducing-adafruit-stemma-qt: - status: 200 - checked: "2025-03-28T10:40:08.446Z" -https://www.adafruit.com/product/2652: - status: 200 - checked: "2025-03-28T10:40:09.994Z" -https://www.adafruit.com/product/3660: - status: 200 - checked: "2025-03-28T10:40:11.182Z" -https://www.adafruit.com/product/1980: - status: 200 - checked: "2025-03-28T10:40:12.339Z" -https://www.adafruit.com/product/4831: - status: 200 - checked: "2025-03-28T10:40:13.342Z" -https://www.adafruit.com/product/3595: - status: 200 - checked: "2025-03-28T10:40:14.478Z" -http://wiki.sunfounder.cc/images/7/72/QMC5883L-Datasheet-1.0.pdf: - status: null - checked: "2025-03-28T10:40:15.488Z" -https://cdn.sparkfun.com/assets/learn_tutorials/9/2/1/AS3935_Datasheet_EN_v2.pdf: - status: 200 - checked: "2025-03-28T10:40:16.264Z" -https://www.gotronic.fr/art-jeu-de-capteurs-meteo-33052.htm: - status: 200 - checked: "2025-03-28T10:40:17.562Z" -https://rainsensors.com/products/rg-11/: - status: 200 - checked: "2025-03-28T10:40:19.557Z" -https://www.adafruit.com/product/4415: - status: 200 - checked: "2025-03-28T10:40:20.695Z" -https://www.adafruit.com/product/380: - status: 200 - checked: "2025-03-28T10:40:21.895Z" -https://www.adafruit.com/product/5626: - status: 200 - checked: "2025-03-28T10:40:22.985Z" -https://www.frandroid.com/marques/samsung/845144_samsung-garantit-desormais-4-ans-de-mises-a-jour-a-ses-galaxy-les-2-pieges-de-cette-annonce: - status: 200 - checked: "2025-03-28T10:40:24.027Z" -https://www.cnetfrance.fr/news/part-de-marche-smartphone-39884221.htm: - status: 200 - checked: "2025-03-28T10:40:24.848Z" -https://www.apple.com/fr/shop/buy-iphone/iphone-14: - status: 200 - checked: "2025-03-28T10:40:25.929Z" -https://www.samsung.com/fr/smartphones/galaxy-s22/buy/: - status: 200 - checked: "2025-03-28T10:40:27.920Z" -https://devices.ubuntu-touch.io/device/x605: - status: 200 - checked: "2025-03-28T10:40:28.746Z" -https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullScreen#security: - status: 200 - checked: "2025-03-28T10:40:30.607Z" -https://matrix.to/#/@arnob79:matrix.org: - status: 200 - checked: "2025-03-28T10:40:31.401Z" -https://arnauld.org/interets/informatique/: - status: 200 - checked: "2025-03-28T10:40:38.035Z" -https://arnauld.org/interets/informatique/posts/birdnetpi/: - status: 200 - checked: "2025-03-28T10:40:44.172Z" -https://www.birdweather.com/birdnetpi: - status: 200 - checked: "2025-03-28T10:40:45.776Z" -https://www.tensorflow.org/: - status: 200 - checked: "2025-03-28T10:40:48.880Z" -https://github.com/mcguirepr89/BirdNET-Pi#introduction: - status: 200 - checked: "2025-03-28T10:40:50.323Z" -https://www.amazon.fr/dp/B087T5H3MQ: - status: 200 - checked: "2025-03-28T10:40:53.913Z" -https://www.amazon.fr/dp/B07175JZ27: - status: 200 - checked: "2025-03-28T10:40:54.622Z" -https://www.raspberrypi.com/documentation/computers/configuration.html#configuring-a-user: - status: 200 - checked: "2025-03-28T10:40:55.682Z" -https://phpsysinfo.github.io/phpsysinfo/: - status: 200 - checked: "2025-03-28T10:40:56.515Z" -https://www.adminer.org/: - status: 200 - checked: "2025-03-28T10:40:57.788Z" -https://fr.wikipedia.org/wiki/TOSLINK: - status: 200 - checked: "2025-03-28T10:40:58.738Z" -https://www.samsung.com/us/support/answer/ANS00085244/: - status: 200 - checked: "2025-03-28T10:40:59.697Z" -https://www.amazon.fr/dp/B07P8RS62S: - status: 200 - checked: "2025-03-28T10:41:02.565Z" -https://www.youtube.com/watch?v=f2picMQC-9E: - status: 200 - checked: "2025-03-28T10:41:04.057Z" -https://www.php-fig.org/psr/: - status: 200 - checked: "2025-03-28T10:41:05.530Z" -https://laravel.com/docs/: - status: 200 - checked: "2025-03-28T10:41:06.623Z" -https://en.wikipedia.org/wiki/Don: - status: 200 - checked: "2025-03-28T10:41:07.328Z" -https://en.wikipedia.org/wiki/KISS_principle: - status: 200 - checked: "2025-03-28T10:41:08.036Z" -https://fr.wikipedia.org/wiki/SOLID_%28informatique%29: - status: 200 - checked: "2025-03-28T10:41:09.213Z" -https://fr.wikipedia.org/wiki/Merise_(informatique: - status: 404 - checked: "2025-03-28T10:41:10.139Z" -https://fr.wikipedia.org/wiki/Dark_pattern: - status: 200 - checked: "2025-03-28T10:41:11.462Z" -https://www.linternaute.fr/dictionnaire/fr/definition/rabatteur/: - status: 200 - checked: "2025-03-28T10:41:12.926Z" -https://twitter.com/taylorotwell: - status: 200 - checked: "2025-03-28T10:41:14.452Z" -https://github.com/nunomaduro/: - status: 200 - checked: "2025-03-28T10:41:15.805Z" -https://freek.dev/: - status: 200 - checked: "2025-03-28T10:41:17.700Z" -https://www.conseil-national.medecin.fr/medecin/devoirs-droits/serment-dhippocrate: - status: 200 - checked: "2025-03-28T10:41:18.868Z" -https://git.dern.ovh/Livres/code-de-deontologie-des-developpeurs: - status: 404 - checked: "2025-03-28T10:41:19.542Z" -https://brew.sh/: - status: 200 - checked: "2025-03-28T10:41:20.374Z" -https://tigervnc.org: - status: 200 - checked: "2025-03-28T10:41:21.368Z" -https://support.apple.com/fr-fr/guide/automator/welcome/mac: - status: 200 - checked: "2025-03-28T10:41:22.663Z" -https://github.com/Steam-Headless/docker-steam-headless: - status: 200 - checked: "2025-03-28T10:41:24.048Z" -https://novnc.com/info.html: - status: 200 - checked: "2025-03-28T10:41:24.950Z" -https://fr.wikipedia.org/wiki/Xvfb: - status: 200 - checked: "2025-03-28T10:41:26.502Z" -https://www.apple.com/fr/home-app/: - status: 200 - checked: "2025-03-28T10:41:27.263Z" -https://www.arsouyes.org/: - status: 200 - checked: "2025-03-28T10:41:27.877Z" -https://www.arsouyes.org/interets/informatique/2023//2023-03-20_Abonnements/: - status: 451 - checked: "2025-03-28T10:41:28.480Z" -https://docs.gitea.io/en-us/external-renderers/: - status: 200 - checked: "2025-03-28T10:41:29.887Z" -https://pandoc.org: - status: 200 - checked: "2025-03-28T10:41:31.112Z" -https://docs.gitea.io/en-us/external-renderers/#example-office-docx: - status: 200 - checked: "2025-03-28T10:41:31.962Z" -https://www.mhonarc.org: - status: 200 - checked: "2025-03-28T10:41:33.679Z" -https://fontlibrary.org/en/font/fantasque-sans-mono: - status: 200 - checked: "2025-03-28T10:41:35.362Z" -https://varnish-cache.org/: - status: 200 - checked: "2025-03-28T10:41:36.812Z" -https://developer.mozilla.org/fr/docs/Web/HTML/Element/details: - status: 200 - checked: "2025-03-28T10:41:38.109Z" -https://fontlibrary.org/en/font/jellee-typeface: - status: 200 - checked: "2025-03-28T10:41:39.638Z" -https://www.ecovacs.com/fr: - status: 200 - checked: "2025-03-28T10:41:41.754Z" -https://www.ecovacs.com/fr/deebot-robotic-vacuum-cleaner/deebot-x1e-omni: - status: 200 - checked: "2025-03-28T10:41:43.025Z" -https://pi-hole.net/: - status: 200 - checked: "2025-03-28T10:41:44.193Z" -https://www.ecovacs.com/fr/deebot-robotic-vacuum-cleaner/deebot-x1e-omni#accessories: - status: 200 - checked: "2025-03-28T10:41:45.498Z" -https://github.com/bmartin5692/bumper: - status: 200 - checked: "2025-03-28T10:41:46.925Z" -https://www.justgeek.fr/tout-savoir-sur-bard-le-chatbot-de-google-108594/: - status: 200 - checked: "2025-03-28T10:41:47.766Z" -https://www.tomsguide.fr/apres-chatgpt-dans-bing-microsoft-ajoute-bing-a-chatgpt-pour-ecraser-google-en-intelligence-artificielle/: - status: 200 - checked: "2025-03-28T10:41:49.124Z" -https://www.forbes.fr/technologie/eric-schmidt-ancien-pdg-de-google-alerte-sur-la-dangerosite-de-lia/: - status: 200 - checked: "2025-03-28T10:41:51.632Z" -https://www.cnbc.com/2023/05/22/bill-gates-predicts-the-big-winner-in-ai-smart-assistants.html: - status: 200 - checked: "2025-03-28T10:41:53.653Z" -https://fr.wikipedia.org/wiki/Grand_modèle_de_langage#Tokénisation: - status: 200 - checked: "2025-03-28T10:41:54.390Z" -https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names:meta-keywords: - status: 200 - checked: "2025-03-28T10:41:55.640Z" -https://fr.wikipedia.org/wiki/Traitement_automatique_des_langues: - status: 200 - checked: "2025-03-28T10:41:56.983Z" -https://openai.com/: - status: 200 - checked: "2025-03-28T10:41:58.376Z" -https://fr.wikipedia.org/wiki/ChatGPT: - status: 200 - checked: "2025-03-28T10:41:59.133Z" -https://www.clubic.com/pro/entreprises/google/actualite-471384-nouveau-google-avec-ia-encore-plus-de-pub-dans-le-moteur-de-recherche-c-est-possible.html: - status: 200 - checked: "2025-03-28T10:42:00.594Z" -https://www.lefigaro.fr/secteur/high-tech/elle-n-est-plus-elle-meme-le-desarroi-des-utilisateurs-de-replika-rejetes-par-leurs-petites-amies-virtuelles-20230319: - status: 200 - checked: "2025-03-28T10:42:01.898Z" -https://www.adagp.fr/fr/actualites/ia-et-droit-dauteur-ladagp-en-appelle-une-regulation-sur-trois-points: - status: 200 - checked: "2025-03-28T10:42:02.839Z" -https://fr.wikipedia.org/wiki/Chaîne_de_Markov: - status: 200 - checked: "2025-03-28T10:42:04.748Z" -https://www.apple.com/fr/newsroom/2023/06/apple-introduces-the-15-inch-macbook-air/: - status: 200 - checked: "2025-03-28T10:42:06.182Z" -https://www.apple.com/fr/newsroom/2023/06/apple-introduces-m2-ultra/: - status: 200 - checked: "2025-03-28T10:42:07.859Z" -https://www.apple.com/fr/newsroom/2023/06/apple-unveils-new-mac-studio-and-brings-apple-silicon-to-mac-pro/: - status: 200 - checked: "2025-03-28T10:42:09.194Z" -https://www.apple.com/fr/newsroom/2023/06/ios-17-makes-iphone-more-personal-and-intuitive/: - status: 200 - checked: "2025-03-28T10:42:10.698Z" -https://www.apple.com/fr/newsroom/2023/06/ipados-17-brings-new-levels-of-personalization-and-versatility-to-ipad/: - status: 200 - checked: "2025-03-28T10:42:12.154Z" -https://www.apple.com/fr/newsroom/2023/06/macos-sonoma-brings-new-capabilities-for-elevating-productivity-and-creativity/: - status: 200 - checked: "2025-03-28T10:42:13.625Z" -https://www.apple.com/fr/newsroom/2023/06/airpods-redefine-the-personal-audio-experience/: - status: 200 - checked: "2025-03-28T10:42:14.619Z" -https://www.apple.com/fr/newsroom/2023/06/tvos-17-brings-facetime-and-video-conferencing-to-apple-tv-4k/: - status: 200 - checked: "2025-03-28T10:42:15.932Z" -https://www.apple.com/fr/newsroom/2023/06/introducing-watchos-10-a-milestone-update-for-apple-watch/: - status: 200 - checked: "2025-03-28T10:42:17.445Z" -https://www.lego.com/fr-fr/product/pirate-ship-playground-40589?icmp=LP-SHSB-Standard-NO_Sidekick_40589_Pirate_Ship_Playrgound_GWP_PP-P-NO-FZLDKPWN4J-1: - status: 200 - checked: "2025-03-28T10:42:20.686Z" -https://www.lego.com/fr-fr/product/velociraptor-escape-76957: - status: 200 - checked: "2025-03-28T10:42:23.261Z" -https://www.lego.com/fr-fr/product/visitor-center-t-rex-raptor-attack-76961: - status: 200 - checked: "2025-03-28T10:42:25.816Z" -https://search.nixos.org/options?channel=23.05&from=0&size=50&sort=relevance&type=packages&query=services.drone: - status: 200 - checked: "2025-03-28T10:42:26.554Z" -https://blog.gitea.io/2023/03/gitea-1.19.0-is-released/: - status: 200 - checked: "2025-03-28T10:42:28.895Z" -https://docs.gitea.com/next/usage/actions/overview: - status: 200 - checked: "2025-03-28T10:42:29.598Z" -https://github.com/marketplace: - status: 200 - checked: "2025-03-28T10:42:31.392Z" -https://docs.gitea.com/next/usage/actions/faq#where-will-the-runner-download-scripts-when-using-actions-such-as-actionscheckoutv3: - status: 200 - checked: "2025-03-28T10:42:32.812Z" -https://docs.gitea.com/next/usage/actions/act-runner: - status: 200 - checked: "2025-03-28T10:42:33.543Z" -https://docs.gitea.com/next/usage/actions/act-runner#labels: - status: 200 - checked: "2025-03-28T10:42:34.264Z" -https://search.nixos.org/options?channel=23.05&show=services.gitea-actions-runner.instances.%3Cname%3E.labels&from=0&size=50&sort=alpha_asc&type=packages&query=gitea: - status: 200 - checked: "2025-03-28T10:42:34.952Z" -https://docs.github.com/en/actions: - status: 200 - checked: "2025-03-28T10:42:36.109Z" -https://github.com/actions/checkout.: - status: 404 - checked: "2025-03-28T10:42:37.208Z" -https://github.com/actions/checkout/tree/main/src: - status: 200 - checked: "2025-03-28T10:42:38.594Z" -https://github.com/actions/checkout/blob/main/LICENSE: - status: 200 - checked: "2025-03-28T10:42:39.934Z" -https://git-lfs.com/: - status: 200 - checked: "2025-03-28T10:42:40.583Z" -https://github.com/peaceiris/actions-hugo: - status: 200 - checked: "2025-03-28T10:42:42.359Z" -https://github.com/actions/setup-node: - status: 200 - checked: "2025-03-28T10:42:43.799Z" -https://docs.gitea.com/next/usage/secrets: - status: 404 - checked: "2025-03-28T10:42:45.218Z" -https://github.com/easingthemes/ssh-deploy@main: - status: 404 - checked: "2025-03-28T10:42:46.302Z" -https://rsync.samba.org/: - status: 200 - checked: "2025-03-28T10:42:47.512Z" -https://github.com/go-gitea/gitea/issues/24721: - status: 200 - checked: "2025-03-28T10:42:49.352Z" -https://woodpecker-ci.org/: - status: 200 - checked: "2025-03-28T10:42:50.810Z" -https://nixos.org: - status: 200 - checked: "2025-03-28T10:42:51.515Z" -https://pop.system76.com: - status: 200 - checked: "2025-03-28T10:42:53.013Z" -https://store.steampowered.com/steamos/buildyourown: - status: 200 - checked: "2025-03-28T10:42:53.948Z" -https://draugeros.org/: - status: 200 - checked: "2025-03-28T10:42:56.078Z" -https://www.drone.io/: - status: 200 - checked: "2025-03-28T10:42:57.329Z" -https:// 0 + ? maxConcurrentConfig + : 4; +const DEFAULT_USER_AGENT = + typeof externalConfig.userAgent === "string" && externalConfig.userAgent.trim() + ? externalConfig.userAgent.trim() + : new UserAgent().toString(); +const ENABLE_COOKIES = externalConfig.enableCookies !== false; +const PROGRESS_FILE = path.join(__dirname, "cache", "external_links_progress.csv"); +const execFileAsync = util.promisify(execFile); + +fs.mkdirSync(CACHE_DIR, { recursive: true }); +if (ENABLE_COOKIES) { + fs.mkdirSync(path.dirname(COOKIE_JAR), { recursive: true }); + if (!fs.existsSync(COOKIE_JAR)) { + fs.closeSync(fs.openSync(COOKIE_JAR, "a")); + } +} + +try { + if (fs.existsSync(PROGRESS_FILE)) { + fs.unlinkSync(PROGRESS_FILE); + } +} catch (error) { + console.warn(`Unable to remove existing progress file: ${error.message}`); +} let cache = {}; if (fs.existsSync(CACHE_PATH)) { cache = yaml.load(fs.readFileSync(CACHE_PATH, "utf8")) || {}; } +let cacheDirty = false; const now = new Date(); const BAD_LINKS = []; +const lastHostChecks = new Map(); +const runResults = new Map(); -function isExternalLink(link) { - return typeof link === "string" && link.includes("://"); +function updateProgress(processed, total) { + process.stdout.write(`\rURL ${processed}/${total}`); } function isCacheValid(entry) { if (!entry?.checked) return false; const date = new Date(entry.checked); - return (now - date) / (1000 * 60 * 60 * 24) < CACHE_TTL_DAYS; + const ttlDays = (() => { + const status = entry.status; + if (typeof status === "number") { + if (status < 400) return CACHE_TTL_SUCCESS_DAYS; + if (status < 500) return CACHE_TTL_CLIENT_ERROR_DAYS; + return 0; + } + return 0; + })(); + if (ttlDays <= 0) return false; + return (now - date) / (1000 * 60 * 60 * 24) < ttlDays; } -function extractLinksFromText(text) { - const regex = /\bhttps?:\/\/[^\s)"'>]+/g; - return text.match(regex) || []; -} - -async function checkLink(file, line, url) { - if (isCacheValid(cache[url])) return; - - const meta = await scrapePage(url, null, { screenshot: false }); - cache[url] = { - status: meta.httpStatus || null, - checked: new Date().toISOString(), - }; - - const bundle = path.relative(SITE_ROOT, file); - - if (!meta.httpStatus || meta.httpStatus >= 400) { - BAD_LINKS.push({ bundle, url, line, status: meta.httpStatus }); - process.stdout.write("❌"); - } else { - process.stdout.write("✔"); +async function collectMarkdownLinks(filePath, occurrencesMap) { + const entries = await collectMarkdownLinksFromFile(filePath); + for (const { url, line } of entries) { + recordOccurrence(occurrencesMap, filePath, line, url); } } -async function processMarkdown(filePath) { - const fileStream = fs.createReadStream(filePath); - const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); - let lineNumber = 0; - for await (const line of rl) { - lineNumber++; - const links = extractLinksFromText(line); - for (const link of links) { - await checkLink(filePath, lineNumber, link); +function recordOccurrence(occurrencesMap, filePath, lineNumber, url) { + if (!occurrencesMap.has(url)) { + occurrencesMap.set(url, { url, occurrences: [] }); + } + const entry = occurrencesMap.get(url); + const alreadyRecorded = entry.occurrences.some( + (item) => item.file === filePath && item.line === lineNumber + ); + if (!alreadyRecorded) { + entry.occurrences.push({ file: filePath, line: lineNumber }); + } +} + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function applyHostDelay(host) { + if (!host) return; + const last = lastHostChecks.get(host); + if (last) { + const elapsed = Date.now() - last; + const waitTime = HOST_DELAY_MS - elapsed; + if (waitTime > 0) { + await delay(waitTime); } } } -function processYamlRecursively(obj, links = []) { - if (typeof obj === "string" && isExternalLink(obj)) { - links.push(obj); +function recordHostCheck(host) { + if (host) { + lastHostChecks.set(host, Date.now()); + } +} + +function extractHost(url) { + try { + return new URL(url).hostname; + } catch (_) { + return null; + } +} + +function persistCache() { + if (!cacheDirty) return; + ensureDirectoryExists(CACHE_PATH); + fs.writeFileSync(CACHE_PATH, yaml.dump(cache)); + cacheDirty = false; +} + +function formatLocations(occurrences) { + return occurrences + .map(({ file, line }) => `${path.relative(SITE_ROOT, file)}:${line}`) + .join("; "); +} + +function escapeCsvField(value) { + const stringValue = String(value); + if (/[",\n]/.test(stringValue)) { + return `"${stringValue.replace(/"/g, '""')}"`; + } + return stringValue; +} + +function appendProgress(url, occurrences, status) { + const locationText = formatLocations(occurrences); + const statusText = + typeof status === "number" && status < 400 && status !== null ? "" : status ?? ""; + const line = [ + escapeCsvField(url), + escapeCsvField(locationText), + escapeCsvField(statusText), + ].join(","); + fs.appendFileSync(PROGRESS_FILE, `${line}\n`); +} + +function groupEntriesByHost(entries) { + const result = new Map(); + for (const entry of entries) { + const host = extractHost(entry.url); + const key = host || `__invalid__:${entry.url}`; + if (!result.has(key)) { + result.set(key, { host, entries: [] }); + } + result.get(key).entries.push(entry); + } + return Array.from(result.values()); +} + +async function runWithConcurrency(items, worker, concurrency) { + const executing = new Set(); + const promises = []; + for (const item of items) { + const promise = Promise.resolve().then(() => worker(item)); + promises.push(promise); + executing.add(promise); + const clean = () => executing.delete(promise); + promise.then(clean).catch(clean); + if (executing.size >= concurrency) { + await Promise.race(executing); + } + } + return Promise.all(promises); +} + +async function curlRequest(url, method) { + const args = [ + "--silent", + "--location", + "--fail", + "--max-time", + `${REQUEST_TIMEOUT_SECONDS}`, + "--output", + "/dev/null", + "--write-out", + "%{http_code}", + "--user-agent", + DEFAULT_USER_AGENT, + "--request", + method, + url, + ]; + + if (ENABLE_COOKIES) { + args.push("--cookie", COOKIE_JAR, "--cookie-jar", COOKIE_JAR); + } + + try { + const { stdout } = await execFileAsync("curl", args); + const status = parseInt(stdout.trim(), 10); + return { + status: Number.isNaN(status) ? null : status, + errorType: null, + method: method.toUpperCase(), + }; + } catch (error) { + const rawStatus = error?.stdout?.toString().trim(); + const status = rawStatus ? parseInt(rawStatus, 10) : null; + const errorCode = Number(error?.code); + const errorType = errorCode === 28 ? "timeout" : null; + return { + status: Number.isNaN(status) ? null : status, + errorType, + method: method.toUpperCase(), + }; + } +} + +function shouldRetryWithGet(result) { + if (result.errorType) return true; + if (result.status === null) return true; + return result.status >= 400; +} + +async function checkLink(url) { + let info = runResults.get(url); + if (!info) { + const cachedInfo = cache[url]; + if (!isCacheValid(cachedInfo)) { + const host = extractHost(url); + if (host) { + await applyHostDelay(host); + } + + let result = await curlRequest(url, "HEAD"); + recordHostCheck(host); + + if (shouldRetryWithGet(result)) { + await delay(RETRY_DELAY_MS); + if (host) { + await applyHostDelay(host); + } + result = await curlRequest(url, "GET"); + recordHostCheck(host); + } + + info = { + status: result.status ?? null, + errorType: result.errorType || null, + method: result.method, + checked: new Date().toISOString(), + }; + cache[url] = info; + cacheDirty = true; + persistCache(); + } else if (cachedInfo) { + info = cachedInfo; + } else { + info = { + status: null, + errorType: "unknown", + method: "HEAD", + checked: new Date().toISOString(), + }; + } + runResults.set(url, info); + } + return info; +} + +function processYamlRecursively(obj, links = new Set()) { + if (typeof obj === "string") { + for (const link of extractLinksFromText(obj)) { + links.add(link); + } } else if (Array.isArray(obj)) { for (const item of obj) processYamlRecursively(item, links); } else if (typeof obj === "object" && obj !== null) { @@ -76,15 +340,117 @@ function processYamlRecursively(obj, links = []) { return links; } -async function processYaml(filePath) { +function stripYamlInlineComment(line) { + let inSingle = false; + let inDouble = false; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (ch === "'" && !inDouble) { + const next = line[i + 1]; + if (inSingle && next === "'") { + i++; + continue; + } + inSingle = !inSingle; + } else if (ch === '"' && !inSingle) { + if (!inDouble) { + inDouble = true; + } else if (line[i - 1] !== "\\") { + inDouble = false; + } + } else if (ch === "#" && !inSingle && !inDouble) { + return line.slice(0, i); + } else if (ch === "\\" && inDouble) { + i++; + } + } + return line; +} + +function isYamlCommentLine(line) { + return line.trim().startsWith("#"); +} + +function isBlockScalarIndicator(line) { + const cleaned = stripYamlInlineComment(line).trim(); + return /:\s*[>|][0-9+-]*\s*$/.test(cleaned); +} + +async function collectYamlLinks(filePath, occurrencesMap) { + let linkSet = new Set(); try { const doc = yaml.load(fs.readFileSync(filePath, "utf8")); - const links = processYamlRecursively(doc); - for (const link of links) { - await checkLink(filePath, "?", link); - } + linkSet = processYamlRecursively(doc); } catch (e) { console.error(`Failed to parse YAML file: ${filePath}`); + return; + } + + if (linkSet.size === 0) return; + + const recorded = new Map(); + const rawLines = fs.readFileSync(filePath, "utf8").split(/\r?\n/); + let inBlockScalar = false; + let blockIndent = 0; + + const markRecorded = (url, lineNumber) => { + if (!recorded.has(url)) { + recorded.set(url, new Set()); + } + const lines = recorded.get(url); + if (lines.has(lineNumber)) return; + lines.add(lineNumber); + recordOccurrence(occurrencesMap, filePath, lineNumber, url); + }; + + for (let index = 0; index < rawLines.length; index++) { + const lineNumber = index + 1; + const line = rawLines[index]; + const indent = line.match(/^\s*/)?.[0].length ?? 0; + const trimmed = line.trim(); + + if (inBlockScalar) { + if (trimmed === "" && indent < blockIndent) { + inBlockScalar = false; + continue; + } + if (trimmed === "" || indent >= blockIndent) { + if (isYamlCommentLine(line)) { + continue; + } + for (const link of extractLinksFromText(line)) { + if (linkSet.has(link)) { + markRecorded(link, lineNumber); + } + } + continue; + } + inBlockScalar = false; + } + + const withoutComment = stripYamlInlineComment(line); + const trimmedWithoutComment = withoutComment.trim(); + + if (isBlockScalarIndicator(line)) { + inBlockScalar = true; + blockIndent = indent + 1; + } + + if (isYamlCommentLine(line)) continue; + + if (!trimmedWithoutComment) continue; + + for (const link of extractLinksFromText(withoutComment)) { + if (linkSet.has(link)) { + markRecorded(link, lineNumber); + } + } + } + + for (const link of linkSet) { + if (!recorded.has(link) || recorded.get(link).size === 0) { + recordOccurrence(occurrencesMap, filePath, "?", link); + } } } @@ -103,24 +469,155 @@ function walk(dir, exts) { return results; } -(async () => { - const mdFiles = walk(CONTENT_DIR, [".md"]); - const yamlFiles = walk(DATA_DIR, [".yaml", ".yml"]); - console.log(`Scanning ${mdFiles.length} Markdown and ${yamlFiles.length} YAML files...`); +function ensureDirectoryExists(targetFile) { + fs.mkdirSync(path.dirname(targetFile), { recursive: true }); +} +function escapeMarkdownCell(value) { + return String(value).replace(/\|/g, "\\|").replace(/\r?\n/g, " "); +} + +function generateMarkdownReport(entries) { + const header = [ + "# Broken External Links", + "", + `Generated: ${new Date().toISOString()}`, + "", + ]; + if (entries.length === 0) { + return header.concat(["No broken external links found."]).join("\n"); + } + const rows = entries.map((entry) => { + const url = escapeMarkdownCell(entry.url); + const location = escapeMarkdownCell(entry.location); + const status = escapeMarkdownCell(entry.status); + return `| ${url} | ${location} | ${status} |`; + }); + return header + .concat(["| URL | Location | Status |", "| --- | --- | --- |", ...rows]) + .join("\n"); +} + +function generateCsvReport(entries) { + const lines = [`"url","location","status"`]; + for (const entry of entries) { + const line = [entry.url, entry.location, entry.status] + .map((field) => `"${String(field).replace(/"/g, '""')}"`) + .join(","); + lines.push(line); + } + return lines.join("\n"); +} + +function writeReport(entries) { + const format = String(externalConfig.outputFormat || "markdown").toLowerCase(); + const content = + format === "csv" ? generateCsvReport(entries) : generateMarkdownReport(entries); + ensureDirectoryExists(OUTPUT_FILE); + fs.writeFileSync(OUTPUT_FILE, content, "utf8"); +} + +(async () => { + const occurrencesByUrl = new Map(); + const mdFiles = walk(CONTENT_DIR, [".md", ".markdown"]); + const yamlFiles = walk(CONTENT_DIR, [".yaml", ".yml"]); for (const file of mdFiles) { - await processMarkdown(file); + await collectMarkdownLinks(file, occurrencesByUrl); } for (const file of yamlFiles) { - await processYaml(file); + await collectYamlLinks(file, occurrencesByUrl); } + const uniqueEntries = Array.from(occurrencesByUrl.values()); + const activeUrls = new Set(uniqueEntries.map((entry) => entry.url)); + let cachePruned = false; + for (const url of Object.keys(cache)) { + if (!activeUrls.has(url)) { + delete cache[url]; + cachePruned = true; + } + } + if (cachePruned) { + cacheDirty = true; + } + ensureDirectoryExists(PROGRESS_FILE); + fs.writeFileSync(PROGRESS_FILE, `"url","locations","status"\n`, "utf8"); + + const total = uniqueEntries.length; + if (total === 0) { + process.stdout.write("No external links found.\n"); + ensureDirectoryExists(CACHE_PATH); + fs.writeFileSync(CACHE_PATH, yaml.dump(cache)); + writeReport([]); + return; + } + + const hostGroups = groupEntriesByHost(uniqueEntries); + const concurrency = Math.max(1, Math.min(MAX_CONCURRENT_HOSTS, hostGroups.length || 1)); + let processed = 0; + await runWithConcurrency( + hostGroups, + async ({ entries }) => { + for (const entry of entries) { + const info = await checkLink(entry.url); + const status = typeof info?.status === "number" ? info.status : null; + const errorType = info?.errorType || null; + const hasHttpError = status !== null && status >= 400; + const isTimeout = errorType === "timeout"; + const statusLabel = isTimeout ? "timeout" : status ?? "error"; + + if (status === null || hasHttpError || isTimeout) { + BAD_LINKS.push({ + location: formatLocations(entry.occurrences), + url: entry.url, + status: statusLabel, + }); + } + + appendProgress(entry.url, entry.occurrences, hasHttpError || isTimeout || status === null ? statusLabel : status); + processed += 1; + updateProgress(processed, total); + } + }, + concurrency + ); + process.stdout.write("\n"); + + ensureDirectoryExists(CACHE_PATH); fs.writeFileSync(CACHE_PATH, yaml.dump(cache)); - console.log("\n\n=== Broken External Links Report ==="); if (BAD_LINKS.length === 0) { - console.log("✅ No broken external links found."); - } else { - console.table(BAD_LINKS); + writeReport([]); + console.log( + `No broken external links detected. Report saved to ${path.relative( + SITE_ROOT, + OUTPUT_FILE + )}.` + ); + return; } + + const sorted = BAD_LINKS.sort((a, b) => { + const rank = (entry) => { + if (entry.status === "timeout") return 2; + if (typeof entry.status === "number") { + return entry.status === 404 ? 0 : 1; + } + return 1; + }; + const diff = rank(a) - rank(b); + if (diff !== 0) return diff; + if (typeof a.status === "number" && typeof b.status === "number") { + return a.status - b.status; + } + return a.url.localeCompare(b.url); + }); + + writeReport(sorted); + console.log( + `Found ${sorted.length} broken external link(s). Report saved to ${path.relative( + SITE_ROOT, + OUTPUT_FILE + )}.` + ); })(); diff --git a/tools/config.json b/tools/config.json index 1b42ba96..8524fb3a 100644 --- a/tools/config.json +++ b/tools/config.json @@ -1,5 +1,19 @@ { "rebrickable": { "apiKey": "" + }, + "externalLinks": { + "cacheDir": "tools/cache", + "cacheFile": "external_links.yaml", + "hostDelayMs": 2000, + "retryDelayMs": 5000, + "requestTimeoutSeconds": 5, + "cacheTtlSuccessDays": 7, + "cacheTtlClientErrorDays": 0, + "outputFormat": "markdown", + "outputFile": "tools/cache/external_links_report.md", + "userAgent": null, + "enableCookies": true, + "cookieJar": "tools/cache/curl_cookies.txt" } -} \ No newline at end of file +} diff --git a/tools/lib/markdown_links.js b/tools/lib/markdown_links.js new file mode 100644 index 00000000..afe1dd9c --- /dev/null +++ b/tools/lib/markdown_links.js @@ -0,0 +1,246 @@ +const fs = require("fs"); +const readline = require("readline"); + +function trimUnbalancedTrailing(value, openChar, closeChar) { + let result = value; + while (result.endsWith(closeChar)) { + const openCount = (result.match(new RegExp(`\\${openChar}`, "g")) || []).length; + const closeCount = (result.match(new RegExp(`\\${closeChar}`, "g")) || []).length; + if (closeCount > openCount) { + result = result.slice(0, -1); + } else { + break; + } + } + return result; +} + +function sanitizeUrlCandidate(raw, options = {}) { + if (typeof raw !== "string") return null; + let candidate = raw.trim(); + if (!candidate) return null; + + if (candidate.startsWith("<") && candidate.endsWith(">")) { + candidate = candidate.slice(1, -1).trim(); + } + + while (/[.,;:!?'"\u2018\u2019\u201C\u201D]+$/.test(candidate)) { + candidate = candidate.slice(0, -1); + } + + if (!options.keepTrailingParens) { + candidate = trimUnbalancedTrailing(candidate, "(", ")"); + } else if (candidate.endsWith(")")) { + const openCount = (candidate.match(/\(/g) || []).length; + const closeCount = (candidate.match(/\)/g) || []).length; + if (closeCount > openCount) { + candidate = trimUnbalancedTrailing(candidate, "(", ")"); + } + } + candidate = trimUnbalancedTrailing(candidate, "[", "]"); + candidate = trimUnbalancedTrailing(candidate, "{", "}"); + + candidate = candidate.replace(/[*_]+$/, ""); + candidate = candidate.replace(/\[\^[^\]]*\]$/, ""); + if (!options.keepTrailingParens) { + candidate = trimUnbalancedTrailing(candidate, "(", ")"); + } + + if ((candidate.match(/\(/g) || []).length > (candidate.match(/\)/g) || []).length) { + return null; + } + if ((candidate.match(/\[/g) || []).length > (candidate.match(/]/g) || []).length) { + return null; + } + if ((candidate.match(/{/g) || []).length > (candidate.match(/}/g) || []).length) { + return null; + } + + return candidate || null; +} + +function findMatchingPair(text, startIndex, openChar, closeChar) { + let depth = 0; + for (let i = startIndex; i < text.length; i++) { + const ch = text[i]; + if (ch === "\\") { + i++; + continue; + } + if (ch === openChar) { + depth++; + } else if (ch === closeChar) { + depth--; + if (depth === 0) { + return i; + } + } + } + return -1; +} + +function parseLinkDestination(raw) { + if (typeof raw !== "string") return null; + let candidate = raw.trim(); + if (!candidate) return null; + + if (candidate.startsWith("<")) { + const closeIndex = candidate.indexOf(">"); + if (closeIndex > 0) { + return candidate.slice(1, closeIndex).trim(); + } + } + + let result = ""; + let escaping = false; + let parenDepth = 0; + for (let i = 0; i < candidate.length; i++) { + const ch = candidate[i]; + if (escaping) { + result += ch; + escaping = false; + continue; + } + if (ch === "\\") { + escaping = true; + continue; + } + if (ch === "(") { + parenDepth++; + } else if (ch === ")" && parenDepth > 0) { + parenDepth--; + } else if (/\s/.test(ch) && parenDepth === 0) { + break; + } + result += ch; + } + return result; +} + +function extractMarkdownDestinations(text) { + const urls = []; + for (let i = 0; i < text.length; i++) { + if (text[i] === "!") { + if (text[i + 1] !== "[") continue; + i += 1; + } + if (text[i] !== "[") continue; + + const closeBracket = findMatchingPair(text, i, "[", "]"); + if (closeBracket === -1) continue; + + let pointer = closeBracket + 1; + while (pointer < text.length && /\s/.test(text[pointer])) pointer++; + if (pointer >= text.length || text[pointer] !== "(") { + i = closeBracket; + continue; + } + + const openParen = pointer; + const closeParen = findMatchingPair(text, openParen, "(", ")"); + if (closeParen === -1) { + break; + } + + const rawDestination = text.slice(openParen + 1, closeParen); + const candidate = parseLinkDestination(rawDestination); + if (candidate) { + urls.push(candidate); + } + i = closeParen; + } + return urls; +} + +function isExternalLink(link) { + return typeof link === "string" && link.includes("://"); +} + +function extractLinksFromText(text) { + if (typeof text !== "string" || !text.includes("http")) { + return []; + } + + const results = []; + const seen = new Set(); + + function addCandidate(candidate, options = {}) { + const sanitized = sanitizeUrlCandidate(candidate, options); + if (!sanitized) return; + if (!isExternalLink(sanitized)) return; + if (seen.has(sanitized)) return; + seen.add(sanitized); + results.push(sanitized); + } + + for (const url of extractMarkdownDestinations(text)) { + addCandidate(url, { keepTrailingParens: true }); + } + + const angleRegex = /<\s*(https?:\/\/[^>\s]+)\s*>/gi; + let match; + while ((match = angleRegex.exec(text)) !== null) { + addCandidate(match[1]); + } + + const autoRegex = /https?:\/\/[^\s<>"`]+/gi; + while ((match = autoRegex.exec(text)) !== null) { + addCandidate(match[0]); + } + + return results; +} + +async function collectMarkdownLinksFromStream(stream) { + const rl = readline.createInterface({ input: stream, crlfDelay: Infinity }); + const results = []; + let lineNumber = 0; + let inFrontMatter = false; + try { + for await (const line of rl) { + lineNumber++; + const trimmed = line.trim(); + + if (lineNumber === 1 && trimmed === "---") { + inFrontMatter = true; + continue; + } + if (inFrontMatter) { + if (trimmed === "---") { + inFrontMatter = false; + continue; + } + if (trimmed.startsWith("#")) { + continue; + } + } + + for (const url of extractLinksFromText(line)) { + results.push({ url, line: lineNumber }); + } + } + } finally { + rl.close(); + if (typeof stream.close === "function") { + stream.close(); + } + } + return results; +} + +async function collectMarkdownLinksFromFile(filePath) { + const stream = fs.createReadStream(filePath, { encoding: "utf8" }); + try { + return await collectMarkdownLinksFromStream(stream); + } catch (error) { + stream.destroy(); + throw error; + } +} + +module.exports = { + collectMarkdownLinksFromFile, + collectMarkdownLinksFromStream, + extractLinksFromText, + sanitizeUrlCandidate, +}; diff --git a/tools/test_archive.js b/tools/tests/archive.test.js similarity index 88% rename from tools/test_archive.js rename to tools/tests/archive.test.js index 5c07f8dc..2d248f89 100644 --- a/tools/test_archive.js +++ b/tools/tests/archive.test.js @@ -1,4 +1,4 @@ -const { getArchiveUrl, saveToArchive } = require("./lib/archive"); +const { getArchiveUrl, saveToArchive } = require("../lib/archive"); (async () => { const testUrl = "https://richard-dern.fr"; diff --git a/tools/tests/markdown_links.test.js b/tools/tests/markdown_links.test.js new file mode 100644 index 00000000..27897bb1 --- /dev/null +++ b/tools/tests/markdown_links.test.js @@ -0,0 +1,68 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { Readable } = require("node:stream"); +const { + collectMarkdownLinksFromStream, + extractLinksFromText, + sanitizeUrlCandidate, +} = require("../lib/markdown_links"); + +test("extractLinksFromText returns sanitized external URLs only once", () => { + const input = + "See [example](https://example.com) and . " + + "Autolink https://bar.com/path).\nDuplicate https://example.com!"; + const urls = extractLinksFromText(input); + assert.deepStrictEqual(urls, ["https://example.com", "https://foo.com", "https://bar.com/path"]); +}); + +test("collectMarkdownLinksFromStream preserves line numbers", async () => { + const content = [ + "Intro line with no link", + "Markdown [link](https://docs.example.org/page).", + "Plain link https://news.example.net/article.", + "Trailing punctuation.", + "Markdown [link](https://docs.example.org/page(with more valid content)).", + "Le **[baume du Canada](https://fr.wikipedia.org/wiki/Baume_du_Canada)**", + "(_Theropoda [incertae sedis](https://fr.wikipedia.org/wiki/Incertae_sedis)_)", + "[CDN](https://fr.wikipedia.org/wiki/Réseau_de_diffusion_de_contenu)[^2]." + ].join("\n"); + const stream = Readable.from([content]); + const links = await collectMarkdownLinksFromStream(stream); + assert.deepStrictEqual(links, [ + { url: "https://docs.example.org/page", line: 2 }, + { url: "https://news.example.net/article", line: 3 }, + { url: "https://portal.example.com/path", line: 4 }, + { url: "https://docs.example.org/page(with more valid content)", line: 5 }, + { url: "https://fr.wikipedia.org/wiki/Baume_du_Canada", line: 6 }, + { url: "https://fr.wikipedia.org/wiki/Incertae_sedis", line: 7 }, + { url: "https://fr.wikipedia.org/wiki/Réseau_de_diffusion_de_contenu", line: 8 }, + ]); +}); + +test("collectMarkdownLinksFromStream ignores URLs in front matter comments", async () => { + const content = [ + "---", + "links:", + " # url: https://ignored.example.com", + " - url: https://included.example.com", + "---", + "Body with https://body.example.com link.", + ].join("\n"); + const stream = Readable.from([content]); + const links = await collectMarkdownLinksFromStream(stream); + assert.deepStrictEqual(links, [ + { url: "https://included.example.com", line: 4 }, + { url: "https://body.example.com", line: 6 }, + ]); +}); + +test("sanitizeUrlCandidate removes spurious trailing punctuation", () => { + const cases = [ + ["https://example.com).", "https://example.com"], + ["https://example.com!\"", "https://example.com"], + ["", "https://example.com"], + ]; + for (const [input, expected] of cases) { + assert.equal(sanitizeUrlCandidate(input), expected); + } +}); diff --git a/tools/test_puppeteer.js b/tools/tests/puppeteer.test.js similarity index 87% rename from tools/test_puppeteer.js rename to tools/tests/puppeteer.test.js index 1c440bfc..4e0fd6a7 100644 --- a/tools/test_puppeteer.js +++ b/tools/tests/puppeteer.test.js @@ -1,4 +1,4 @@ -const { scrapePage } = require("./lib/puppeteer"); +const { scrapePage } = require("../lib/puppeteer"); const path = require("path"); (async () => {