diff --git a/apps/web/package.json b/apps/web/package.json
index 708aeda..9b7dabd 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -12,8 +12,9 @@
"test": "vitest run"
},
"dependencies": {
- "@civic-source/types": "workspace:*",
+ "@astrojs/rss": "^4.0.18",
"@astrojs/svelte": "^8.0.4",
+ "@civic-source/types": "workspace:*",
"@octokit/rest": "^22.0.1",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2",
diff --git a/apps/web/src/layouts/BaseLayout.astro b/apps/web/src/layouts/BaseLayout.astro
index a2fdcc1..1b02698 100644
--- a/apps/web/src/layouts/BaseLayout.astro
+++ b/apps/web/src/layouts/BaseLayout.astro
@@ -38,6 +38,7 @@ const titleEntries = Object.entries(TITLE_NAMES)
+
diff --git a/apps/web/src/pages/feed.xml.ts b/apps/web/src/pages/feed.xml.ts
new file mode 100644
index 0000000..cb13f67
--- /dev/null
+++ b/apps/web/src/pages/feed.xml.ts
@@ -0,0 +1,29 @@
+import rss from '@astrojs/rss';
+import { getCollection } from 'astro:content';
+import type { APIContext } from 'astro';
+
+export async function GET(context: APIContext) {
+ const statutes = await getCollection('statutes');
+
+ // Sort by generated_at descending to get most recently updated sections
+ const sorted = statutes
+ .filter(s => s.data.generated_at)
+ .sort((a, b) => {
+ const aDate = a.data.generated_at ?? '';
+ const bDate = b.data.generated_at ?? '';
+ return bDate.localeCompare(aDate);
+ })
+ .slice(0, 50);
+
+ return rss({
+ title: 'US Code Tracker โ Recent Updates',
+ description: 'Track amendments to the United States Code. Updated weekly from the Office of the Law Revision Counsel.',
+ site: context.site ?? 'https://civic-source.github.io/us-code-tracker/',
+ items: sorted.map(entry => ({
+ title: `${entry.data.usc_title} U.S.C. ยง ${entry.data.usc_section} โ ${entry.data.title.replace(/^Section \S+ - /, '')}`,
+ pubDate: new Date(entry.data.generated_at ?? Date.now()),
+ link: `/us-code-tracker/statute/${entry.id}/`,
+ description: `${entry.data.classification}. Current through ${entry.data.current_through}.`,
+ })),
+ });
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 339730d..5ca99f5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -47,6 +47,9 @@ importers:
apps/web:
dependencies:
+ '@astrojs/rss':
+ specifier: ^4.0.18
+ version: 4.0.18
'@astrojs/svelte':
specifier: ^8.0.4
version: 8.0.4(@types/node@25.5.0)(astro@6.1.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.3))(jiti@2.6.1)(lightningcss@1.32.0)(svelte@5.55.0)(typescript@5.9.3)(yaml@2.8.3)
@@ -256,6 +259,9 @@ packages:
resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==}
engines: {node: '>=22.12.0'}
+ '@astrojs/rss@4.0.18':
+ resolution: {integrity: sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg==}
+
'@astrojs/svelte@8.0.4':
resolution: {integrity: sha512-c5m3chjtgxBE3BzsE/bZbCFBkLPhq041rm2WJFaTIKGwt/3xNm/5efYCj23reuAcBsl4iYS8n2UwkAHQJzhkZA==}
engines: {node: '>=22.12.0'}
@@ -3158,6 +3164,12 @@ snapshots:
dependencies:
prismjs: 1.30.0
+ '@astrojs/rss@4.0.18':
+ dependencies:
+ fast-xml-parser: 5.5.9
+ piccolore: 0.1.3
+ zod: 4.3.6
+
'@astrojs/svelte@8.0.4(@types/node@25.5.0)(astro@6.1.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.3))(jiti@2.6.1)(lightningcss@1.32.0)(svelte@5.55.0)(typescript@5.9.3)(yaml@2.8.3)':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))