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
73 lines
2.8 KiB
JavaScript
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
|
|
// 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",
|
|
},
|
|
});
|
|
}
|