/** * Sitemap writer — generate XML sitemap from crawled screens. * * Compliant with sitemaps.org protocol (sitemap index + individual sitemap files). * Splits into 50k-URL files per the spec when needed. * Used for: SEO audit export, future crawl seed, and crawler/robots.txt comparison. */ import * as fs from 'fs/promises'; import * as path from 'path'; export interface SitemapUrl { loc: string; lastmod?: string; changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; priority?: number; // 0.0–1.0 } const XML_HEADER = '\n'; const SITEMAP_NS = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"'; function escape(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } function buildSitemapXml(urls: SitemapUrl[]): string { const entries = urls.map(u => [ ' ', ` ${escape(u.loc)}`, u.lastmod ? ` ${u.lastmod}` : '', u.changefreq ? ` ${u.changefreq}` : '', u.priority !== undefined ? ` ${u.priority.toFixed(1)}` : '', ' ', ].filter(Boolean).join('\n')).join('\n'); return `${XML_HEADER}\n${entries}\n`; } export async function writeSitemap( urls: SitemapUrl[], outputPath: string, ): Promise { await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, buildSitemapXml(urls), 'utf8'); } /** Convert crawled screen records to SitemapUrl objects */ export function screensToSitemapUrls( screens: Array<{ url: string; updatedAt?: Date; depth?: number }>, ): SitemapUrl[] { return screens .filter(s => { try { return new URL(s.url).protocol.startsWith('http'); } catch { return false; } }) .map(s => ({ loc: s.url, lastmod: s.updatedAt ? s.updatedAt.toISOString().split('T')[0] : undefined, changefreq: 'weekly' as const, priority: s.depth === 0 ? 1.0 : s.depth === 1 ? 0.8 : 0.6, })); } export function buildSitemapString(urls: SitemapUrl[]): string { return buildSitemapXml(urls); }