import rss from '@astrojs/rss'; import type { APIRoute } from 'astro'; import ogDefault from '../assets/og-default.jpg'; import { absoluteUrl, articlePath, entrySlug, 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, '''); } // Format a Date as `YYYY-MM-DD` in UTC for use inside tag: URIs. function isoDate(date: Date) { return date.toISOString().slice(0, 10); } export const GET: APIRoute = async (context) => { const posts = await getPublishedPosts(); const feedUrl = absoluteUrl('/rss.xml'); const channelImage = await optimizeOgImage(ogDefault); const channelImageUrl = absoluteUrl(channelImage.src); const creator = escapeXml(site.name); return rss({ title: site.name, description: site.description, site: context.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: posts.map((post) => { const url = absoluteUrl(articlePath(post)); // Stable tag: URI keeps the GUID constant across path renames // (e.g. the `/writing/` → `/articles/` migration). The date is the // original publish date so re-publishing won't change the GUID. const guid = `tag:schmelczer.dev,${isoDate(post.data.date)}:posts/${entrySlug(post)}`; const updated = post.data.updated ? `${post.data.updated.toISOString()}` : ''; return { title: post.data.title, description: post.data.description, pubDate: post.data.date, link: url, author: `${site.email} (${site.name})`, categories: [...post.data.tags], customData: [ `${escapeXml(guid)}`, `${creator}`, updated, ] .filter(Boolean) .join('\n'), }; }), }); };