/** * Dead link detector — tracks broken links discovered during crawl. * * A "dead link" is a URL that: * - Returns 404, 410, or other 4xx/5xx responses * - Times out after maxRetries * - Redirects to an error page (detected by title heuristic) * * Results: list of broken links with source page, status code, and anchor text. * Used for: SEO audit export, test report enrichment. */ export interface DeadLink { url: string; statusCode: number; sourceUrl?: string; anchorText?: string; checkedAt: string; reason: 'http-error' | 'timeout' | 'dns-error'; } export class DeadLinkTracker { private deadLinks: DeadLink[] = []; private checked = new Map(); // url → statusCode record( url: string, statusCode: number, source?: { sourceUrl?: string; anchorText?: string }, ): void { if (this.checked.has(url)) return; this.checked.set(url, statusCode); if (statusCode >= 400) { this.deadLinks.push({ url, statusCode, sourceUrl: source?.sourceUrl, anchorText: source?.anchorText, checkedAt: new Date().toISOString(), reason: statusCode === 0 ? 'timeout' : 'http-error', }); } } recordTimeout(url: string, sourceUrl?: string): void { if (this.checked.has(url)) return; this.checked.set(url, 0); this.deadLinks.push({ url, statusCode: 0, sourceUrl, checkedAt: new Date().toISOString(), reason: 'timeout' }); } getDeadLinks(): DeadLink[] { return [...this.deadLinks]; } isKnownBroken(url: string): boolean { const code = this.checked.get(url); return code !== undefined && code >= 400; } stats(): { total: number; by404: number; by5xx: number; byTimeout: number } { return { total: this.deadLinks.length, by404: this.deadLinks.filter(l => l.statusCode === 404).length, by5xx: this.deadLinks.filter(l => l.statusCode >= 500).length, byTimeout: this.deadLinks.filter(l => l.reason === 'timeout').length, }; } toCsv(): string { const rows = [['URL', 'Status', 'Source URL', 'Anchor Text', 'Checked At']]; for (const l of this.deadLinks) { rows.push([l.url, String(l.statusCode), l.sourceUrl ?? '', l.anchorText ?? '', l.checkedAt]); } return rows.map(r => r.map(c => `"${c.replace(/"/g, '""')}"`).join(',')).join('\n'); } }