blog.git

blog

~ Personal research notes. blog.sqzer.com
git clone https://github.com/debuman/blog.git

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
     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 }