feat: added rss endpoint
fa76e1ff
5 file(s) · +66 −2
| 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 | ||
| 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": { |
| 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) => { |
|
| 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"; |
| 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; |