← back to build-pages
feed: Atom 1.0 per project (/p/:slug/feed.atom) + autodiscovery <link> + 4 smoke
c196cfdf167d54420b44bbbdfb151a0b86e67c55 · 2026-05-13 13:30:50 -0700 · SteveStudio2
Files touched
M server.jsM test/smoke.js
Diff
commit c196cfdf167d54420b44bbbdfb151a0b86e67c55
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 13:30:50 2026 -0700
feed: Atom 1.0 per project (/p/:slug/feed.atom) + autodiscovery <link> + 4 smoke
---
server.js | 40 ++++++++++++++++++++++++++++++++++++++++
test/smoke.js | 8 ++++++++
2 files changed, 48 insertions(+)
diff --git a/server.js b/server.js
index 7498286..9b68cf1 100644
--- a/server.js
+++ b/server.js
@@ -403,6 +403,8 @@ app.get('/p/:slug', async (req, res, next) => {
// Apply the initial ?q= from the server-rendered value field.
if (q.value) applyFilter();
</script>
+ ` + `
+ <link rel="alternate" type="application/atom+xml" title="${esc(p.name)} build journal" href="/p/${esc(req.params.slug)}/feed.atom">
`, {
canonical: `https://projects.agentabrams.com/p/${req.params.slug}`,
desc: `${p.name} — ${p.blurb}`,
@@ -432,6 +434,44 @@ app.get('/p/:slug/c/:hash', async (req, res, next) => {
} catch (e) { next(e); }
});
+// Atom feed per project — recent N commits in standards-compliant Atom 1.0.
+// Lets external tools subscribe to a project's build journal (e.g. Feedly,
+// NetNewsWire). Pulls from the same bundle cache as the HTML page.
+app.get('/p/:slug/feed.atom', async (req, res, next) => {
+ const p = PROJECTS[req.params.slug];
+ if (!p) return res.status(404).type('application/xml').send('<error>no-such-project</error>');
+ try {
+ const { commits } = await getProjectBundle(req.params.slug, p);
+ const base = 'https://projects.agentabrams.com';
+ const entries = commits.slice(0, 50).map(c => {
+ const url = `${base}/p/${req.params.slug}/c/${c.shortHash}`;
+ const body = c.body ? `\n\n${c.body}` : '';
+ return ` <entry>
+ <id>${url}</id>
+ <title>${esc(c.subject)}</title>
+ <link href="${url}"/>
+ <updated>${new Date(c.dateISO).toISOString()}</updated>
+ <author><name>${esc(c.author)}</name></author>
+ <content type="text">${esc(c.subject + body)}</content>
+ </entry>`;
+ }).join('\n');
+ const updated = commits[0] ? new Date(commits[0].dateISO).toISOString() : new Date().toISOString();
+ const feed = `<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>${base}/p/${req.params.slug}/feed.atom</id>
+ <title>${esc(p.name)} — build journal</title>
+ <link rel="self" href="${base}/p/${req.params.slug}/feed.atom"/>
+ <link href="${base}/p/${req.params.slug}"/>
+ <updated>${updated}</updated>
+ <subtitle>${esc(p.blurb)}</subtitle>
+${entries}
+</feed>
+`;
+ res.setHeader('Cache-Control', 'public, max-age=300, stale-while-revalidate=600');
+ res.type('application/atom+xml').send(feed);
+ } catch (e) { next(e); }
+});
+
// File viewer — show any tracked path at HEAD.
app.get('/p/:slug/file', async (req, res, next) => {
const p = PROJECTS[req.params.slug];
diff --git a/test/smoke.js b/test/smoke.js
index 2f61f56..11a5787 100644
--- a/test/smoke.js
+++ b/test/smoke.js
@@ -125,6 +125,14 @@ function check(name, cond, hint = '') {
check('gzip /p/butlr', (r.headers['content-encoding'] || '') === 'gzip');
check('vary Accept-Encoding', (r.headers['vary'] || '').toLowerCase().includes('accept-encoding'));
+ // 11. Atom feed per project + autodiscovery link in <head>
+ r = await fetch('/p/butlr/feed.atom');
+ check('feed: 200', r.status === 200);
+ check('feed: atom xmlns', /xmlns="http:\/\/www.w3.org\/2005\/Atom"/.test(r.body));
+ check('feed: at least 1 entry', /<entry>/.test(r.body));
+ r = await fetch('/p/butlr');
+ check('butlr: feed autodiscover',/rel="alternate" type="application\/atom\+xml"/.test(r.body));
+
// Report
if (process.argv.includes('--json')) {
console.log(JSON.stringify({ base: BASE, pass, fail, total: pass + fail, checks, as_of: new Date().toISOString() }));
← cb50486 smoke: gzip /p/butlr + Vary: Accept-Encoding (compression re
·
back to build-pages
·
api: /api/projects/:slug/commits — paginated raw JSON (limit 254d1d0 →