A fullstack app for indexing standard.site documents
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: added rss endpoint

Steve fa76e1ff e6cf1b99

+66 -2
+7
bun.lock
··· 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
+2 -1
packages/server/package.json
··· 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": {
+2 -1
packages/server/src/index.ts
··· 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) => {
+1
packages/server/src/routes/index.ts
··· 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";
+54
packages/server/src/routes/rss.ts
··· 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;