Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
3.9 kB · 98 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899/** * The updates feed. * * Hand-written rather than pulled from a package: the whole of Atom this * needs is one entry per post, the frontmatter already carries every field, * and a feed is a thing you have to be able to read to trust. * * Atom rather than RSS because the collection's dates map onto it exactly — * `published` never moves and `updated` is what `updatedDate` means — and * because every entry must carry an id, so a reader can tell a retitled post * from a new one. * * Every URL is absolute against `site` in astro.config.mjs: a feed is read * somewhere else by definition, and a relative href in one resolves against * the reader, which is nowhere. */
import type { APIContext } from "astro";import { getCollection } from "astro:content";
/** Where readers subscribe. Also spelled out in Layout.astro's <link>. */export const FEED_PATH = "/atom.xml";
// The five characters that are not text in XML. Everything below goes// through here — a post title is written by a human in a markdown file, and// an ampersand in one would otherwise end the document at that byte.function xml(text: string): string { return text .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'");}
export async function GET(context: APIContext): Promise<Response> { const site = context.site; if (!site) throw new Error("astro.config.mjs has no site; the feed needs it");
const url = (path: string): string => new URL(path, site).href;
// The same rule /posts uses, so a reader and a visitor never disagree // about what has been posted. const posts = ( await getCollection( "posts", ({ data }) => import.meta.env.DEV || !data.draft, ) ).sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());
const stamp = (date: Date): string => date.toISOString(); const changed = (post: (typeof posts)[number]): Date => post.data.updatedDate ?? post.data.publishDate;
// The feed's own timestamp is the newest post's, never the build's: a feed // stamped at build time reports news on a day nothing was written. With no // posts, the epoch is the honest answer. const updated = posts.length ? changed(posts[0]!) : new Date(0);
const entries = posts.map((post) => { const href = url(`/posts/${post.id}`); return [ " <entry>", ` <title>${xml(post.data.title)}</title>`, ` <link rel="alternate" type="text/html" href="${xml(href)}"/>`, // The address is the id: stable for as long as the post is at it, // which is what trailingSlash: "never" and the canonical link already // promise everywhere else. ` <id>${xml(href)}</id>`, ` <published>${stamp(post.data.publishDate)}</published>`, ` <updated>${stamp(changed(post))}</updated>`, ` <summary type="text">${xml(post.data.description)}</summary>`, ...post.data.tags.map((tag) => ` <category term="${xml(tag)}"/>`), " </entry>", ].join("\n"); });
const document = [ '<?xml version="1.0" encoding="utf-8"?>', '<feed xmlns="http://www.w3.org/2005/Atom">', // The same string Layout.astro's autodiscovery link carries. " <title>substandard updates</title>", ` <id>${xml(url(FEED_PATH))}</id>`, ` <updated>${stamp(updated)}</updated>`, // rel="self" is how a reader handed this document knows where to fetch // it again; the alternate is where a human should be sent. ` <link rel="self" type="application/atom+xml" href="${xml(url(FEED_PATH))}"/>`, ` <link rel="alternate" type="text/html" href="${xml(url("/posts"))}"/>`, " <author><name>substandard</name></author>", ...entries, "</feed>", "", ].join("\n");
return new Response(document, { headers: { "content-type": "application/atom+xml; charset=utf-8" }, });}