import rss from '@astrojs/rss'; import type { APIRoute } from 'astro'; import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import { render } from 'astro:content'; import ogDefault from '../assets/og-default.jpg'; import { absoluteUrl, articlePath, getPublishedPosts, optimizeOgImage, site, } from '../lib/site'; // Escape characters that would otherwise break XML parsing inside text nodes // (the `customData` strings are inserted as-is by @astrojs/rss). function escapeXml(value: string) { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Rewrite root-relative URLs to absolute so RSS readers (which load the HTML // outside any page context) can still resolve assets and links. function absolutizeUrls(html: string, baseUrl: string) { return html .replace(/(<(?:a|link)\b[^>]*\bhref=")(\/[^"]*)(")/g, `$1${baseUrl}$2$3`) .replace( /(<(?:img|source|video|audio)\b[^>]*\bsrc=")(\/[^"]*)(")/g, `$1${baseUrl}$2$3` ) .replace(/(\bsrcset=")([^"]+)(")/g, (_, prefix, value, suffix) => { const rewritten = value .split(',') .map((candidate: string) => { const trimmed = candidate.trim(); if (!trimmed.startsWith('/')) return trimmed; return baseUrl + trimmed; }) .join(', '); return prefix + rewritten + suffix; }); } export const GET: APIRoute = async () => { const posts = await getPublishedPosts(); const feedUrl = absoluteUrl('/rss.xml'); const channelImage = await optimizeOgImage(ogDefault); const channelImageUrl = absoluteUrl(channelImage.src); const creator = escapeXml(site.name); const container = await AstroContainer.create(); const items = await Promise.all( posts.map(async (post) => { const url = absoluteUrl(articlePath(post)); const updated = post.data.updated ? `${post.data.updated.toISOString()}` : ''; const { Content } = await render(post); const html = await container.renderToString(Content); // @astrojs/rss XML-escapes the `content` string and emits it inside // . RSS readers decode the escaped HTML the same as if // it were wrapped in CDATA, so escaping is fine and safer to author. const content = absolutizeUrls(html, site.url); return { title: post.data.title, description: post.data.description, pubDate: post.data.date, link: url, author: `${site.email} (${site.name})`, categories: [...post.data.tags], content, customData: [`${creator}`, updated] .filter(Boolean) .join('\n'), }; }) ); return rss({ title: site.name, description: site.description, site: site.url, xmlns: { atom: 'http://www.w3.org/2005/Atom', content: 'http://purl.org/rss/1.0/modules/content/', dc: 'http://purl.org/dc/elements/1.1/', }, customData: [ 'en-us', `${new Date().toUTCString()}`, ``, '', ` ${channelImageUrl}`, ` ${escapeXml(site.name)}`, ` ${site.url}`, '', ].join('\n'), items, }); };