/**
* 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);
}