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 format RFC 822 date function formatRFC822Date(date: Date): string { return date.toUTCString() } export const GET: RequestHandler = async (event) => { try { // Get published photography albums const albums = 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: 50 // Limit to most recent 50 albums }) // Get individual published photos not in albums const standalonePhotos = await prisma.photo.findMany({ where: { status: 'published', showInPhotos: true, albumId: null }, orderBy: { publishedAt: 'desc' }, take: 25 }) // Combine albums and standalone photos const items = [ ...albums.map((album) => ({ type: 'album', 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}`, pubDate: album.createdAt, updatedDate: album.updatedAt, guid: `${event.url.origin}/photos/${album.slug}`, photoCount: album._count.photos, coverPhoto: album.photos[0], location: album.location, date: album.date })), ...standalonePhotos.map((photo) => ({ type: 'photo', id: photo.id.toString(), title: photo.title || photo.filename, description: photo.description || photo.caption || `Photo: ${photo.filename}`, content: photo.description ? `

${escapeXML(photo.description)}

` : photo.caption ? `

${escapeXML(photo.caption)}

` : '', link: `${event.url.origin}/photos/photo/${photo.slug || photo.id}`, pubDate: photo.publishedAt || photo.createdAt, updatedDate: photo.updatedAt, guid: `${event.url.origin}/photos/photo/${photo.slug || photo.id}`, url: photo.url, thumbnailUrl: photo.thumbnailUrl })) ].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 = ` Photos - jedmund.com Photography and visual content from jedmund ${event.url.origin}/photos 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.type} ${ item.type === 'album' && item.coverPhoto ? ` ` : '' } ${ item.type === 'photo' ? ` ` : '' } ${item.location ? `${escapeXML(item.location)}` : ''} noreply@jedmund.com (Justin Edmund) ` ) .join('')} ` logger.info('Photos 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 Photos RSS feed', error as Error) return new Response('Failed to generate RSS feed', { status: 500 }) } }