/** * RSS Feed Generation Utilities */ export interface RSSChannelConfig { title: string; link: string; description: string; language?: string; selfLink?: string; copyright?: string; managingEditor?: string; webMaster?: string; generator?: string; ttl?: number; } export interface RSSItem { title: string; link: string; guid?: string; pubDate: Date | string; description?: string; content?: string; author?: string; categories?: string[]; enclosure?: { url: string; length?: number; type?: string; }; comments?: string; source?: { url: string; title: string; }; } export function escapeXml(unsafe: string): string { return unsafe.replace(/&/g, '&').replace(//g, '>'); } export function escapeXmlAttribute(unsafe: string): string { return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } export function normalizeCharacters(text: string): string { return text .replace(/\u2018|\u2019|\u201A|\u201B/g, "'") .replace(/\u201C|\u201D|\u201E|\u201F/g, '"') .replace(/\u2013/g, '-') .replace(/\u2014/g, '--') .replace(/\u00A0/g, ' ') .replace(/\u2026/g, '...') .replace(/\u2022/g, '*') .replace(/'/g, "'") .replace(/"/g, '"') .replace(/ /g, ' ') .replace(/—/g, '--') .replace(/–/g, '-') .replace(/…/g, '...') .replace(/’/g, "'") .replace(/‘/g, "'") .replace(/”/g, '"') .replace(/“/g, '"'); } export function formatRSSDate(date: Date | string): string { const d = typeof date === 'string' ? new Date(date) : date; return d.toUTCString(); } export function generateRSSItem(item: RSSItem): string { const guid = item.guid || item.link; const pubDate = formatRSSDate(item.pubDate); const title = escapeXml(normalizeCharacters(item.title)); const description = item.description ? escapeXml(normalizeCharacters(item.description)) : ''; const content = item.content ? normalizeCharacters(item.content) : ''; const author = item.author ? escapeXml(normalizeCharacters(item.author)) : ''; const categories = item.categories ?.map((cat) => ` ${escapeXml(normalizeCharacters(cat))}`) .join('\n') || ''; let enclosure = ''; if (item.enclosure) { const length = item.enclosure.length ? ` length="${item.enclosure.length}"` : ''; const type = item.enclosure.type ? ` type="${escapeXmlAttribute(item.enclosure.type)}"` : ''; enclosure = ` `; } let source = ''; if (item.source) { source = ` ${escapeXml(normalizeCharacters(item.source.title))}`; } return ` ${title} ${escapeXmlAttribute(item.link)} ${escapeXmlAttribute(guid)} ${pubDate}${description ? `\n ${description}` : ''}${content ? `\n ` : ''}${author ? `\n ${author}` : ''}${item.comments ? `\n ${escapeXmlAttribute(item.comments)}` : ''}${categories ? `\n${categories}` : ''}${enclosure ? `\n${enclosure}` : ''}${source ? `\n${source}` : ''} `; } export function generateRSSFeed(config: RSSChannelConfig, items: RSSItem[]): string { const language = config.language || 'en'; const generator = config.generator || 'SvelteKit with AT Protocol'; const lastBuildDate = formatRSSDate(new Date()); const title = escapeXml(normalizeCharacters(config.title)); const link = escapeXmlAttribute(config.link); const description = escapeXml(normalizeCharacters(config.description)); const generatorText = escapeXml(normalizeCharacters(generator)); const atomLink = config.selfLink ? ` ` : ''; const optionalFields = []; if (config.copyright) optionalFields.push( ` ${escapeXml(normalizeCharacters(config.copyright))}` ); if (config.managingEditor) optionalFields.push( ` ${escapeXml(normalizeCharacters(config.managingEditor))}` ); if (config.webMaster) optionalFields.push( ` ${escapeXml(normalizeCharacters(config.webMaster))}` ); if (config.ttl) optionalFields.push(` ${config.ttl}`); const itemsXml = items.map((item) => generateRSSItem(item)).join('\n'); return ` ${title} ${link} ${description} ${language}${atomLink ? `\n${atomLink}` : ''} ${lastBuildDate} ${generatorText}${optionalFields.length > 0 ? `\n${optionalFields.join('\n')}` : ''} ${itemsXml} `; } export function createRSSResponse( feed: string, options?: { cacheMaxAge?: number; status?: number } ): Response { const cacheMaxAge = options?.cacheMaxAge ?? 3600; const status = options?.status ?? 200; return new Response(feed, { status, headers: { 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': `public, max-age=${cacheMaxAge}` } }); }