diff --git a/bun.lock b/bun.lock index f68e1c2..74f7fc4 100644 --- a/bun.lock +++ b/bun.lock @@ -25,6 +25,7 @@ "name": "@document-feeds/server", "version": "1.0.0", "dependencies": { + "feed": "^4.2.2", "hono": "^4.0.0", }, "devDependencies": { @@ -318,6 +319,8 @@ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "feed": ["feed@4.2.2", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -380,6 +383,8 @@ "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -420,6 +425,8 @@ "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=="], + "xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="], + "xp.css": ["xp.css@0.2.6", "", {}, "sha512-v0qUB4wOvyXIMLNid5bIpm+0n5aL4i8wE5dqJH8LNVxE04PxUUxjwf2rNzengegUIK03XC4vbfLVCPAfMLtuqA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], diff --git a/packages/server/package.json b/packages/server/package.json index 515409f..1c5dc6d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,10 +3,11 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "wrangler dev", + "dev": "wrangler dev --remote", "deploy": "wrangler deploy --minify" }, "dependencies": { + "feed": "^4.2.2", "hono": "^4.0.0" }, "devDependencies": { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 6d452c5..63c8bf8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import type { Bindings } from "./types"; -import { health, webhook, feed, stats, records, admin } from "./routes"; +import { health, webhook, feed, stats, records, admin, rss } from "./routes"; import { processDocument } from "./utils"; const app = new Hono<{ Bindings: Bindings }>(); @@ -16,6 +16,7 @@ app.route("/feed", feed); app.route("/stats", stats); app.route("/records", records); app.route("/admin", admin); +app.route("/rss", rss); // 404 handler app.notFound((c) => { diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 5fbf8c4..c2e93a4 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -4,3 +4,4 @@ export { default as feed } from "./feed"; export { default as stats } from "./stats"; export { default as records } from "./records"; export { default as admin } from "./admin"; +export { default as rss } from "./rss"; diff --git a/packages/server/src/routes/rss.ts b/packages/server/src/routes/rss.ts new file mode 100644 index 0000000..5433a2f --- /dev/null +++ b/packages/server/src/routes/rss.ts @@ -0,0 +1,54 @@ +import { Hono } from "hono"; +import { Feed } from "feed"; +import type { Bindings, ResolvedDocumentRow } from "../types"; + +const rss = new Hono<{ Bindings: Bindings }>(); + +rss.get("/", async (c) => { + try { + const db = c.env.DB; + + const { results } = await db + .prepare( + `SELECT uri, did, rkey, title, description, path, site, content, text_content, + cover_image_cid, cover_image_url, bsky_post_ref, tags, + published_at, updated_at, pub_url, pub_name, pub_description, + pub_icon_cid, pub_icon_url, view_url, pds_endpoint, + resolved_at, stale_at, verified + FROM resolved_documents + WHERE verified = 1 + ORDER BY published_at DESC + LIMIT 50`, + ) + .all(); + + const feed = new Feed({ + title: "Docs.surf", + description: "A feed of standard.site documents to surf", + id: "https://docs.surf/", + link: "https://docs.surf/", + copyright: "", + }); + + for (const row of results || []) { + feed.addItem({ + title: row.title || "Untitled", + id: row.uri, + link: row.view_url || row.uri, + description: row.description || undefined, + date: row.published_at ? new Date(row.published_at) : new Date(), + }); + } + + return c.body(feed.rss2(), 200, { + "Content-Type": "application/xml", + }); + } catch (error) { + return c.json( + { error: "Failed to generate RSS feed", details: String(error) }, + 500, + ); + } +}); + +export default rss;