[object Object]

← 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

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 →