src/pages/index.xml.ts (2459B)
1 import rss from '@astrojs/rss';
2 import { getCollection } from 'astro:content';
3 import type { APIContext } from 'astro';
4 import { byHugoOrder, postUrl } from '../lib/order';
5 import { excerpt } from '../lib/prose';
6
7 /**
8 * The feed, at the one address the site has always claimed, and the one every page's
9 * <head> advertises: Base.astro prints `<link rel="alternate" type="application/rss+xml"
10 * title="debuman" href="/index.xml">` on every page, so this file is what
11 * answers a reader who subscribes from any of them.
12 *
13 * One feed, not the per-section feeds the Hugo site shipped: a blog that publishes into
14 * a single stream has nothing to split.
15 *
16 * Ordering and addresses come from src/lib/order.ts rather than being restated here, so
17 * the feed cannot drift from the Writing index. Descriptions reuse the same excerpt()
18 * the page's own <meta name="description"> uses, which means a post that gains a `deck`
19 * improves both at once.
20 *
21 * XML escaping is @astrojs/rss's, not ours. The corpus is full of the characters that
22 * break hand-rolled XML: ampersands in shell pipelines, angle brackets in log output,
23 * quotes and em-dashes in Korean prose.
24 *
25 * HTML escaping is ours. A reader XML-decodes <description> and then renders it as HTML,
26 * but the excerpt is plain text: a first paragraph quoting `<img src=x onerror=…>` in a
27 * code span would come out of the reader as a live element. Escaping & < > before
28 * @astrojs/rss escapes again makes the reader show it as the text it is.
29 */
30 const asHtmlText = (s: string): string => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
31 export async function GET(context: APIContext) {
32 const posts = (await getCollection('writing'))
33 .filter((p) => !p.data.draft)
34 .sort(byHugoOrder);
35
36 return rss({
37 title: 'debuman',
38 description: 'Notes on systems, networks, and the things around them.',
39 // context.site is astro.config.mjs's `site`. Required: every link in a feed is absolute.
40 site: context.site!,
41 items: posts.map((p) => ({
42 title: p.data.title,
43 // Dates are stored YYYY-MM-DD and read as UTC everywhere on this site; parsing them
44 // in local time would move every pubDate back a day on a build machine east of UTC.
45 pubDate: new Date(`${p.data.date}T00:00:00Z`),
46 description: asHtmlText(p.data.deck ?? excerpt(p.body ?? '')),
47 link: postUrl(p),
48 })),
49 customData: '<language>en</language>',
50 });
51 }