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',
},
});
};