93 lines
2.6 KiB
TypeScript
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 => ({
|
|
'<': '<',
|
|
'>': '>',
|
|
'&': '&',
|
|
"'": ''',
|
|
'"': '"',
|
|
}[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',
|
|
},
|
|
});
|
|
};
|