Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

92 linesCodeBlameRaw
1/**
2 * The status page's feeds: every public incident update and maintenance
3 * notice, newest first, as Atom (`/feed.xml`) and JSON Feed
4 * (`/feed.json`). No Workers imports, so it is tested under Node.
5 */
6import type { StatusIncident, StatusMaintenance } from "@g1t/contracts";
7
8import { INCIDENT_STATUS } from "./model.ts";
9
10export type FeedItem = {
11 /** Unique and stable: the page's address with the update's anchor. */
12 id: string;
13 url: string;
14 title: string;
15 text: string;
16 at: string;
17};
18
19/** How many items a feed carries. */
20export const FEED_SIZE = 50;
21
22/** Feed items from incidents and maintenance, newest first. */
23export function feedItems(incidents: StatusIncident[], maintenance: StatusMaintenance[], size = FEED_SIZE): FeedItem[] {
24 const items: FeedItem[] = [];
25 for (const incident of incidents) {
26 for (const update of incident.updates) {
27 items.push({
28 id: `${incident.url}#update-${update.id}`,
29 url: `${incident.url}#update-${update.id}`,
30 title: `${INCIDENT_STATUS[update.status]}: ${incident.title}`,
31 text: update.text,
32 at: update.at,
33 });
34 }
35 }
36 for (const m of maintenance) {
37 for (const update of m.updates) {
38 items.push({
39 id: `${m.url}#update-${update.id}`,
40 url: `${m.url}#update-${update.id}`,
41 title: `Maintenance: ${m.title}`,
42 text: update.text,
43 at: update.at,
44 });
45 }
46 }
47 return items.sort((a, b) => b.at.localeCompare(a.at)).slice(0, size);
48}
49
50function xml(text: string): string {
51 return text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]!);
52}
53
54export type FeedOptions = { origin: string; title: string; updated: string };
55
56export function atom(items: FeedItem[], { origin, title, updated }: FeedOptions): string {
57 const entries = items
58 .map(
59 (item) => `<entry>
60<id>${xml(item.id)}</id>
61<title>${xml(item.title)}</title>
62<link rel="alternate" type="text/html" href="${xml(item.url)}"/>
63<updated>${xml(item.at)}</updated>
64<published>${xml(item.at)}</published>
65<content type="text">${xml(item.text)}</content>
66</entry>`,
67 )
68 .join("\n");
69 return `<?xml version="1.0" encoding="utf-8"?>
70<feed xmlns="http://www.w3.org/2005/Atom">
71<id>${xml(`${origin}/`)}</id>
72<title>${xml(title)}</title>
73<link rel="self" type="application/atom+xml" href="${xml(`${origin}/feed.xml`)}"/>
74<link rel="alternate" type="text/html" href="${xml(`${origin}/`)}"/>
75<updated>${xml(items[0]?.at ?? updated)}</updated>
76<author><name>g1t</name></author>
77${entries}
78</feed>
79`;
80}
81
82/** JSON Feed 1.1. */
83export function jsonFeed(items: FeedItem[], { origin, title }: FeedOptions): object {
84 return {
85 version: "https://jsonfeed.org/version/1.1",
86 title,
87 home_page_url: `${origin}/`,
88 feed_url: `${origin}/feed.json`,
89 language: "en",
90 items: items.map((item) => ({ id: item.id, url: item.url, title: item.title, content_text: item.text, date_published: item.at })),
91 };
92}