seedproject-web/app/src/pages/rss.xml.js
Carlos Arias 1559ce017d chore: scaffold SeedProject base (Phase 1)
Clean-room copy of the reusable engines from comiida, with all
instance data, secrets, dependencies, and build output excluded:
- app/         Astro theme skeleton (no comiida blog posts; hero image -> placeholder)
- api/         SeedProject PHP framework (no vendor/.env/config.php)
- content-pipeline/  engine only (scripts/admin/prompts; empty runtime state)
- astroagent.config.json + app/.astroagent/skills

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 22:53:10 +00:00

73 lines
2.8 KiB
JavaScript

import { getCollection } from "astro:content";
import { SITE, getAuthor, sortedPosts } from "../lib/blog-data.js";
const BASE_URL = SITE.url || "";
const esc = (value) =>
value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
// Wrap arbitrary text safely in a CDATA section.
const cdata = (s) => `<![CDATA[${String(s).replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
// Convert MDX/Markdown body to plain text (text only — strip formatting, keep link text).
function mdToText(md = "") {
return md
.replace(/^---[\s\S]*?---\s*/, "") // strip frontmatter if present
.replace(/^import\s.*$/gm, "") // strip MDX import lines
.replace(/`{1,3}([^`]*)`{1,3}/g, "$1") // inline code
.replace(/!\[[^\]]*\]\([^)]*\)/g, "") // images
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") // links → text
.replace(/^\s{0,3}#{1,6}\s+/gm, "") // headings
.replace(/^\s{0,3}>\s?/gm, "") // blockquotes
.replace(/^\s{0,3}([-*+])\s+/gm, "") // unordered list markers
.replace(/^\s{0,3}\d+\.\s+/gm, "") // ordered list markers
.replace(/^\s*[-*_]{3,}\s*$/gm, "") // horizontal rules
.replace(/(\*\*|__)(.*?)\1/g, "$2") // bold
.replace(/(\*|_)(.*?)\1/g, "$2") // italic
.replace(/<[^>]+>/g, "") // any stray HTML/JSX tags
.replace(/\n{3,}/g, "\n\n") // collapse blank lines
.trim();
}
export async function GET() {
const bodyBySlug = Object.fromEntries((await getCollection("blog")).map((e) => [e.id, e.body || ""]));
const items = (await sortedPosts()).map((post) => {
const link = `${BASE_URL}/blog/${post.slug}`;
const author = getAuthor(post.author);
const fullText = mdToText(bodyBySlug[post.slug]);
return [
" <item>",
` <title>${esc(post.title)}</title>`,
` <link>${link}</link>`,
` <guid isPermaLink="true">${link}</guid>`,
` <pubDate>${new Date(post.date).toUTCString()}</pubDate>`,
author ? ` <dc:creator>${esc(author.name)}</dc:creator>` : "",
` <description>${esc(post.excerpt)}</description>`,
` <content:encoded>${cdata(fullText)}</content:encoded>`,
" </item>",
]
.filter(Boolean)
.join("\n");
});
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">',
" <channel>",
` <title>${esc(SITE.name)}</title>`,
` <link>${BASE_URL}/</link>`,
` <description>${esc(SITE.description)}</description>`,
" <language>en-us</language>",
...items,
" </channel>",
"</rss>",
].join("\n");
return new Response(xml, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
});
}