import type { APIRoute } from 'astro'; interface PostInfo { slug: string; date: string; title?: string; summary?: string; excerpt?: string; tags: string[]; draft: boolean; } interface SiteConfig { title: string; subtitle: string; } // Strip C0/DEL control chars that are illegal in XML 1.0 (allow tab, LF, CR). // eslint-disable-next-line no-control-regex const XML_INVALID = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g; function escapeXml(s: string): string { return s.replace(XML_INVALID, '').replace(/[<>&'"]/g, c => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"', }[c]!)); } function formatSlug(slug: string): string { return slug .split('-') .map(w => w.charAt(0).toUpperCase() + w.slice(1)) .join(' '); } export const GET: APIRoute = async ({ site }) => { const API_URL = process.env.PUBLIC_API_URL || 'http://backend:3000'; const origin = site?.toString().replace(/\/$/, '') || ''; let posts: PostInfo[] = []; let config: SiteConfig = { title: "Ela's Atelier", subtitle: 'Works on paper, canvas, and elsewhere' }; try { const [pr, cr] = await Promise.all([ fetch(`${API_URL}/api/posts`), fetch(`${API_URL}/api/config`), ]); if (pr.ok) posts = await pr.json(); if (cr.ok) config = await cr.json(); } catch (e) { console.error('feed.xml backend fetch failed', e); } const items = posts .filter(p => !p.draft) .map(p => { const url = `${origin}/posts/${encodeURIComponent(p.slug)}`; const description = p.summary || p.excerpt || ''; const pubDate = new Date(p.date).toUTCString(); const categories = p.tags.map(t => ` ${escapeXml(t)}`).join('\n'); return ` ${escapeXml(p.title || formatSlug(p.slug))} ${escapeXml(url)} ${escapeXml(url)} ${pubDate} ${escapeXml(description)} ${categories} `; }) .join('\n'); const xml = ` ${escapeXml(config.title)} ${escapeXml(origin)} ${escapeXml(config.subtitle)} en ${items} `; return new Response(xml, { headers: { 'Content-Type': 'application/xml; charset=utf-8', 'Cache-Control': 'public, max-age=600', }, }); };