/* * Prüft jeden internen Verweis einer Website auf seine Antwort. * * Ein Link, der ins Leere zeigt, ist der Fehler, den niemand bemerkt: Tests prüfen, was der Code * erzeugt, nicht ob die Adresse dahinter existiert. Im Nachbau von kant-it.de zeigten vier * Verweise im Hero-Slider auf Adressen, die es nie gab - gefunden hat das kein Test, sondern ein * Durchlauf wie dieser. * * Ausgangspunkt ist die Sitemap, danach wird jeder Seite gefolgt, die von dort erreichbar ist. * Geprüft werden Seiten, Bilder, Stylesheets und Skripte; externe Adressen werden gezählt, aber * nicht abgerufen - ob fremde Server antworten, ist nicht die Frage dieser Prüfung. * * CMS_URL=http://127.0.0.1:8080 npm run test:links */ import { chromium } from 'playwright'; import assert from 'node:assert/strict'; import { mkdir, writeFile } from 'node:fs/promises'; const origin = (process.env.CMS_URL || 'http://127.0.0.1:8080').replace(/\/$/, ''); const browser = await chromium.launch(); const geprueft = new Map(); const extern = new Set(); const kaputt = []; const quellen = new Map(); try { const context = await browser.newContext(); const page = await context.newPage(); const antwort = async (url) => { if (!geprueft.has(url)) { const response = await context.request.get(url, { maxRedirects: 5 }); geprueft.set(url, response.status()); } return geprueft.get(url); }; const sitemap = await context.request.get(origin + '/sitemap.xml'); assert.equal(sitemap.status(), 200, 'Ohne Sitemap fehlt der Ausgangspunkt.'); const seiten = [...(await sitemap.text()).matchAll(/([^<]+)<\/loc>/g)] .map(treffer => new URL(treffer[1]).pathname); console.log(`Sitemap: ${seiten.length} Adresse(n).`); const besucht = new Set(); const warteschlange = ['/', ...seiten]; while (warteschlange.length > 0) { const pfad = warteschlange.shift(); if (besucht.has(pfad)) continue; besucht.add(pfad); const response = await page.goto(origin + pfad, { waitUntil: 'domcontentloaded' }); if (200 !== response.status()) { kaputt.push({ status: response.status(), url: pfad, von: 'Sitemap' }); continue; } // Verweise und eingebundene Dateien derselben Website. const verweise = await page.evaluate(() => ({ links: [...document.querySelectorAll('a[href]')].map(a => a.getAttribute('href')), dateien: [ ...[...document.querySelectorAll('img[src]')].map(i => i.getAttribute('src')), ...[...document.querySelectorAll('img[srcset]')].flatMap(i => i.getAttribute('srcset').split(',').map(teil => teil.trim().split(/\s+/)[0])), ...[...document.querySelectorAll('link[rel="stylesheet"], link[rel="preload"]')].map(l => l.getAttribute('href')), ...[...document.querySelectorAll('script[src]')].map(s => s.getAttribute('src')), ...[...document.querySelectorAll('[style*="url("]')].flatMap(e => [...e.getAttribute('style').matchAll(/url\(["']?([^"')]+)/g)].map(m => m[1])), ], stile: [...document.querySelectorAll('style')].flatMap(s => [...s.textContent.matchAll(/url\(["']?([^"')]+)/g)].map(m => m[1])), })); for (const roh of [...verweise.links, ...verweise.dateien, ...verweise.stile]) { if (!roh || roh.startsWith('data:') || roh.startsWith('#') || roh.startsWith('mailto:') || roh.startsWith('tel:')) continue; const ziel = new URL(roh, origin + pfad); if (ziel.origin !== origin) { extern.add(ziel.origin); continue; } const adresse = ziel.pathname + ziel.search; const status = await antwort(origin + adresse); if (status >= 400) { kaputt.push({ status, url: adresse, von: pfad }); } // Eigenen Seiten folgen wir weiter; Dateien nicht. if (verweise.links.includes(roh) && !/\.[a-z0-9]{2,5}(\?|$)/i.test(ziel.pathname) && !besucht.has(ziel.pathname)) { warteschlange.push(ziel.pathname); quellen.set(ziel.pathname, pfad); } } } await mkdir('reports', { recursive: true }); await writeFile('reports/links.json', JSON.stringify({ origin, geprueft: geprueft.size, seiten: besucht.size, extern: [...extern], kaputt }, null, 2)); console.log(`${besucht.size} Seite(n), ${geprueft.size} Adresse(n) geprüft, ${extern.size} fremde Quelle(n): ${[...extern].join(', ') || 'keine'}`); if (kaputt.length > 0) { for (const eintrag of kaputt) { console.error(` ${eintrag.status} ${eintrag.url} (verlinkt auf ${eintrag.von})`); } throw new Error(`${kaputt.length} Verweis(e) zeigen ins Leere.`); } console.log('Kein toter Verweis.'); } finally { await browser.close(); }