feat: added rss endpoint fa76e1ff
Steve · 2026-02-01 20:13 5 file(s) · +66 −2
bun.lock +7 −0
25 25
      "name": "@document-feeds/server",
26 26
      "version": "1.0.0",
27 27
      "dependencies": {
28 +
        "feed": "^4.2.2",
28 29
        "hono": "^4.0.0",
29 30
      },
30 31
      "devDependencies": {
318 319
319 320
    "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
320 321
322 +
    "feed": ["feed@4.2.2", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ=="],
323 +
321 324
    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
322 325
323 326
    "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
380 383
381 384
    "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="],
382 385
386 +
    "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
387 +
383 388
    "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
384 389
385 390
    "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
419 424
    "wrangler": ["wrangler@3.114.16", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/unenv-preset": "2.0.2", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250718.3", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.14", "workerd": "1.20250718.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250408.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-ve/ULRjrquu5BHNJ+1T0ipJJlJ6pD7qLmhwRkk0BsUIxatNe4HP4odX/R4Mq/RHG6LOnVAFs7SMeSHlz/1mNlQ=="],
420 425
421 426
    "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
427 +
428 +
    "xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
422 429
423 430
    "xp.css": ["xp.css@0.2.6", "", {}, "sha512-v0qUB4wOvyXIMLNid5bIpm+0n5aL4i8wE5dqJH8LNVxE04PxUUxjwf2rNzengegUIK03XC4vbfLVCPAfMLtuqA=="],
424 431
packages/server/package.json +2 −1
3 3
	"version": "1.0.0",
4 4
	"private": true,
5 5
	"scripts": {
6 -
		"dev": "wrangler dev",
6 +
		"dev": "wrangler dev --remote",
7 7
		"deploy": "wrangler deploy --minify"
8 8
	},
9 9
	"dependencies": {
10 +
		"feed": "^4.2.2",
10 11
		"hono": "^4.0.0"
11 12
	},
12 13
	"devDependencies": {
packages/server/src/index.ts +2 −1
1 1
import { Hono } from "hono";
2 2
import { cors } from "hono/cors";
3 3
import type { Bindings } from "./types";
4 -
import { health, webhook, feed, stats, records, admin } from "./routes";
4 +
import { health, webhook, feed, stats, records, admin, rss } from "./routes";
5 5
import { processDocument } from "./utils";
6 6
7 7
const app = new Hono<{ Bindings: Bindings }>();
16 16
app.route("/stats", stats);
17 17
app.route("/records", records);
18 18
app.route("/admin", admin);
19 +
app.route("/rss", rss);
19 20
20 21
// 404 handler
21 22
app.notFound((c) => {
packages/server/src/routes/index.ts +1 −0
4 4
export { default as stats } from "./stats";
5 5
export { default as records } from "./records";
6 6
export { default as admin } from "./admin";
7 +
export { default as rss } from "./rss";
packages/server/src/routes/rss.ts (added) +54 −0
1 +
import { Hono } from "hono";
2 +
import { Feed } from "feed";
3 +
import type { Bindings, ResolvedDocumentRow } from "../types";
4 +
5 +
const rss = new Hono<{ Bindings: Bindings }>();
6 +
7 +
rss.get("/", async (c) => {
8 +
	try {
9 +
		const db = c.env.DB;
10 +
11 +
		const { results } = await db
12 +
			.prepare(
13 +
				`SELECT uri, did, rkey, title, description, path, site, content, text_content,
14 +
                cover_image_cid, cover_image_url, bsky_post_ref, tags,
15 +
                published_at, updated_at, pub_url, pub_name, pub_description,
16 +
                pub_icon_cid, pub_icon_url, view_url, pds_endpoint,
17 +
                resolved_at, stale_at, verified
18 +
         FROM resolved_documents
19 +
         WHERE verified = 1
20 +
         ORDER BY published_at DESC
21 +
         LIMIT 50`,
22 +
			)
23 +
			.all<ResolvedDocumentRow>();
24 +
25 +
		const feed = new Feed({
26 +
			title: "Docs.surf",
27 +
			description: "A feed of standard.site documents to surf",
28 +
			id: "https://docs.surf/",
29 +
			link: "https://docs.surf/",
30 +
			copyright: "",
31 +
		});
32 +
33 +
		for (const row of results || []) {
34 +
			feed.addItem({
35 +
				title: row.title || "Untitled",
36 +
				id: row.uri,
37 +
				link: row.view_url || row.uri,
38 +
				description: row.description || undefined,
39 +
				date: row.published_at ? new Date(row.published_at) : new Date(),
40 +
			});
41 +
		}
42 +
43 +
		return c.body(feed.rss2(), 200, {
44 +
			"Content-Type": "application/xml",
45 +
		});
46 +
	} catch (error) {
47 +
		return c.json(
48 +
			{ error: "Failed to generate RSS feed", details: String(error) },
49 +
			500,
50 +
		);
51 +
	}
52 +
});
53 +
54 +
export default rss;