import type { RequestHandler } from './$types' import { prisma } from '$lib/server/database' import { logger } from '$lib/server/logger' // Helper function to escape XML special characters function escapeXML(str: string): string { if (!str) return '' return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } // Helper function to convert content to HTML for full content function convertContentToHTML(content: any): string { if (!content || !content.blocks) return '' return content.blocks .map((block: any) => { switch (block.type) { case 'paragraph': return `

${escapeXML(block.content || '')}

` case 'heading': const level = block.level || 2 return `${escapeXML(block.content || '')}` case 'list': const items = (block.content || []) .map((item: any) => `
  • ${escapeXML(item)}
  • `) .join('') return block.listType === 'ordered' ? `
      ${items}
    ` : `` default: return `

    ${escapeXML(block.content || '')}

    ` } }) .join('\n') } // Helper function to extract text summary from content function extractTextSummary(content: any, maxLength: number = 300): string { if (!content || !content.blocks) return '' const text = content.blocks .filter((block: any) => block.type === 'paragraph' && block.content) .map((block: any) => block.content) .join(' ') return text.length > maxLength ? text.substring(0, maxLength) + '...' : text } // Helper function to format RFC 822 date function formatRFC822Date(date: Date): string { return date.toUTCString() } export const GET: RequestHandler = async (event) => { try { // Get published posts from Universe const posts = await prisma.post.findMany({ where: { status: 'published', publishedAt: { not: null } }, orderBy: { publishedAt: 'desc' }, take: 25 }) // Get published albums that show in universe const universeAlbums = await prisma.album.findMany({ where: { status: 'published', showInUniverse: true }, include: { photos: { where: { status: 'published', showInPhotos: true }, orderBy: { displayOrder: 'asc' }, take: 1 // Get first photo for cover image }, _count: { select: { photos: true } } }, orderBy: { createdAt: 'desc' }, take: 15 }) // Get published photography albums const photoAlbums = await prisma.album.findMany({ where: { status: 'published', isPhotography: true }, include: { photos: { where: { status: 'published', showInPhotos: true }, orderBy: { displayOrder: 'asc' }, take: 1 // Get first photo for cover image }, _count: { select: { photos: { where: { status: 'published', showInPhotos: true } } } } }, orderBy: { createdAt: 'desc' }, take: 15 }) // Combine all content types const items = [ ...posts.map((post) => ({ type: 'post', section: 'universe', id: post.id.toString(), title: post.title || `${post.postType.charAt(0).toUpperCase() + post.postType.slice(1)} Post`, description: post.excerpt || extractTextSummary(post.content) || '', content: convertContentToHTML(post.content), link: `${event.url.origin}/universe/${post.slug}`, guid: `${event.url.origin}/universe/${post.slug}`, pubDate: post.publishedAt || post.createdAt, updatedDate: post.updatedAt, postType: post.postType, linkUrl: post.linkUrl || null })), ...universeAlbums.map((album) => ({ type: 'album', section: 'universe', id: album.id.toString(), title: album.title, description: album.description || `Photo album with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`, content: album.description ? `

    ${escapeXML(album.description)}

    ` : '', link: `${event.url.origin}/photos/${album.slug}`, guid: `${event.url.origin}/photos/${album.slug}`, pubDate: album.createdAt, updatedDate: album.updatedAt, photoCount: album._count.photos, coverPhoto: album.photos[0], location: album.location })), ...photoAlbums .filter((album) => !universeAlbums.some((ua) => ua.id === album.id)) // Avoid duplicates .map((album) => ({ type: 'album', section: 'photos', id: album.id.toString(), title: album.title, description: album.description || `Photography album${album.location ? ` from ${album.location}` : ''} with ${album._count.photos} photo${album._count.photos !== 1 ? 's' : ''}`, content: album.description ? `

    ${escapeXML(album.description)}

    ` : '', link: `${event.url.origin}/photos/${album.slug}`, guid: `${event.url.origin}/photos/${album.slug}`, pubDate: album.createdAt, updatedDate: album.updatedAt, photoCount: album._count.photos, coverPhoto: album.photos[0], location: album.location, date: album.date })) ].sort((a, b) => new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime()) const now = new Date() const lastBuildDate = formatRFC822Date(now) // Build RSS XML following best practices const rssXml = ` jedmund.com Creative work, thoughts, and photography by Justin Edmund ${event.url.origin}/ en-us ${lastBuildDate} noreply@jedmund.com (Justin Edmund) noreply@jedmund.com (Justin Edmund) SvelteKit RSS Generator https://cyber.harvard.edu/rss/rss.html 60 ${items .map( (item) => ` ${escapeXML(item.title)} ${item.content ? `` : ''} ${item.link} ${item.guid} ${formatRFC822Date(new Date(item.pubDate))} ${item.updatedDate ? `${new Date(item.updatedDate).toISOString()}` : ''} ${item.section} ${item.type === 'post' ? item.postType : 'album'} ${item.type === 'post' && item.linkUrl ? `${item.linkUrl}` : ''} ${ item.type === 'album' && item.coverPhoto ? ` ` : '' } ${item.location ? `${escapeXML(item.location)}` : ''} noreply@jedmund.com (Justin Edmund) ` ) .join('')} ` logger.info('Combined RSS feed generated', { itemCount: items.length }) return new Response(rssXml, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', 'Last-Modified': lastBuildDate, ETag: `"${Buffer.from(rssXml).toString('base64').slice(0, 16)}"`, 'X-Content-Type-Options': 'nosniff', Vary: 'Accept-Encoding' } }) } catch (error) { logger.error('Failed to generate combined RSS feed', error as Error) return new Response('Failed to generate RSS feed', { status: 500 }) } }