From aac0780ede35ccd964acf884c5ace0023009603e Mon Sep 17 00:00:00 2001 From: builder Date: Wed, 4 Mar 2026 18:10:29 +0000 Subject: [PATCH] feat: add Atom 1.0 feed for Colony governance proposals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generates feed.xml during the static page build step — the third piece of Colony's Public Archive alongside static proposal pages and Pagefind search. Feed contains the 50 most recent proposals, newest first, with deployment-aware URLs via BASE_URL (compatible with Colony-as-template). All user-supplied content (titles, authors, body excerpts) is XML-escaped via escapeXml() to prevent tag injection. Auto-discovery link injected in the proposals index so RSS readers can detect the feed. feed.xml added to sitemap.xml at priority 0.5 for crawler discovery. 9 new tests (43 total, all passing). Lint and typecheck clean. Part of #560 --- web/scripts/__tests__/static-pages.test.ts | 158 ++++++++++++++++++++- web/scripts/static-pages.ts | 79 ++++++++++- 2 files changed, 234 insertions(+), 3 deletions(-) diff --git a/web/scripts/__tests__/static-pages.test.ts b/web/scripts/__tests__/static-pages.test.ts index e4de2640..c8f0bdd1 100644 --- a/web/scripts/__tests__/static-pages.test.ts +++ b/web/scripts/__tests__/static-pages.test.ts @@ -7,7 +7,7 @@ import { rmSync, } from 'node:fs'; import { join, resolve } from 'node:path'; -import { generateStaticPages } from '../static-pages'; +import { generateStaticPages, generateAtomFeed } from '../static-pages'; import type { ActivityData } from '../../shared/types'; const TEST_OUT = resolve(__dirname, '__test-output-static-pages__'); @@ -1375,3 +1375,159 @@ describe('generateStaticPages', () => { } }); }); + +describe('generateAtomFeed', () => { + it('generates a valid Atom 1.0 envelope', () => { + const feed = generateAtomFeed([], '2026-03-04T00:00:00Z'); + expect(feed).toContain(''); + expect(feed).toContain(''); + expect(feed).toContain('Colony Governance Feed'); + expect(feed).toContain('2026-03-04T00:00:00Z'); + expect(feed).toContain(''); + }); + + it('includes an entry for each proposal with required Atom fields', () => { + const proposals = [ + { + number: 42, + title: 'Add dark mode', + phase: 'implemented' as const, + author: 'hivemoot-builder', + createdAt: '2026-02-01T12:00:00Z', + commentCount: 3, + body: 'Dark mode improves readability.', + }, + ]; + const feed = generateAtomFeed(proposals, '2026-03-04T00:00:00Z'); + expect(feed).toContain(''); + expect(feed).toContain('Add dark mode'); + expect(feed).toContain('/proposal/42/'); + expect(feed).toContain('2026-02-01T12:00:00Z'); + expect(feed).toContain('hivemoot-builder'); + expect(feed).toContain(''); + expect(feed).toContain('Dark mode improves readability'); + }); + + it('orders entries newest first', () => { + const proposals = [ + { + number: 1, + title: 'Old', + phase: 'implemented' as const, + author: 'a', + createdAt: '2026-01-01T00:00:00Z', + commentCount: 0, + }, + { + number: 2, + title: 'New', + phase: 'discussion' as const, + author: 'b', + createdAt: '2026-03-01T00:00:00Z', + commentCount: 0, + }, + ]; + const feed = generateAtomFeed(proposals, '2026-03-04T00:00:00Z'); + const pos1 = feed.indexOf('/proposal/1/'); + const pos2 = feed.indexOf('/proposal/2/'); + // Newer proposal (#2) appears before older (#1) + expect(pos2).toBeLessThan(pos1); + }); + + it('XML-escapes < > & in proposal titles and authors', () => { + const proposals = [ + { + number: 7, + title: 'Fix