Files
narlblog/frontend/src/pages/feed.xml.ts
T
2026-05-14 08:24:41 +02:00

93 lines
2.6 KiB
TypeScript

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 => ({
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
"'": '&apos;',
'"': '&quot;',
}[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 => ` <category>${escapeXml(t)}</category>`).join('\n');
return ` <item>
<title>${escapeXml(p.title || formatSlug(p.slug))}</title>
<link>${escapeXml(url)}</link>
<guid isPermaLink="true">${escapeXml(url)}</guid>
<pubDate>${pubDate}</pubDate>
<description>${escapeXml(description)}</description>
${categories}
</item>`;
})
.join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escapeXml(config.title)}</title>
<link>${escapeXml(origin)}</link>
<description>${escapeXml(config.subtitle)}</description>
<language>en</language>
<atom:link href="${escapeXml(origin)}/feed.xml" rel="self" type="application/rss+xml" />
${items}
</channel>
</rss>`;
return new Response(xml, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=600',
},
});
};