diff --git a/src/pages/index.xml.ts b/src/pages/index.xml.ts
@@ -19,10 +19,16 @@ import { excerpt } from '../lib/prose';
* the page's own <meta name="description"> uses, which means a post that gains a `deck`
* improves both at once.
*
- * Escaping is @astrojs/rss's, not ours. The corpus is full of the characters that break
- * hand-rolled XML: ampersands in shell pipelines, angle brackets in log output, quotes
- * and em-dashes in Korean prose.
+ * XML escaping is @astrojs/rss's, not ours. The corpus is full of the characters that
+ * break hand-rolled XML: ampersands in shell pipelines, angle brackets in log output,
+ * quotes and em-dashes in Korean prose.
+ *
+ * HTML escaping is ours. A reader XML-decodes <description> and then renders it as HTML,
+ * but the excerpt is plain text: a first paragraph quoting `<img src=x onerror=…>` in a
+ * code span would come out of the reader as a live element. Escaping & < > before
+ * @astrojs/rss escapes again makes the reader show it as the text it is.
*/
+const asHtmlText = (s: string): string => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
export async function GET(context: APIContext) {
const posts = (await getCollection('writing'))
.filter((p) => !p.data.draft)
@@ -38,7 +44,7 @@ export async function GET(context: APIContext) {
// Dates are stored YYYY-MM-DD and read as UTC everywhere on this site; parsing them
// in local time moves half the corpus back a day.
pubDate: new Date(`${p.data.date}T00:00:00Z`),
- description: p.data.deck ?? excerpt(p.body ?? ''),
+ description: asHtmlText(p.data.deck ?? excerpt(p.body ?? '')),
link: postUrl(p),
})),
customData: '<language>en</language>',
|