From 499bba129c842c00c1a28d85bc044ab609aee44e Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Mon, 21 Sep 2026 17:55:06 -0400 Subject: Add discussions Discussions are served from GitHub's older server-rendered HTML, with no embeddedData JSON to read, so the scraper walks the DOM the way the pull request and wiki code does. A discussion's timeline opens with its own post and continues with its comments, each carrying its replies nested inside it, which is where the recursion in _discussionComment.eta comes from. Categories, upvotes, and the answer a Q&A discussion is marked with all come along. Only what GitHub renders up front is shown: neither the next page of discussions nor the replies it hides behind a "Show N previous replies" button are fetched, so a comment says how many replies it is missing. Co-Authored-By: Claude Opus 5 --- src/app.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'src/app.ts') diff --git a/src/app.ts b/src/app.ts index 0b9a7e6..ec3680b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,8 @@ import { buildCommit } from "./build.generated.ts"; import { getGitHubBlob, getGitHubCommits, + getGitHubDiscussion, + getGitHubDiscussions, getGitHubIssues, getGitHubSidebar, getGitHubLatestCommit, @@ -218,6 +220,28 @@ export function createApp(eta: Eta) { return c.render("pulls.eta", await getGitHubPulls(owner, repo, { q })); }); + app.get("/:owner/:repo/discussions", async (c) => { + const { owner, repo } = c.req.param(); + const q = c.req.query("discussions_q"); + return c.render("discussions.eta", await getGitHubDiscussions(owner, repo, { q })); + }); + + app.get("/:owner/:repo/discussions/categories/:category", async (c) => { + const { owner, repo, category } = c.req.param(); + const q = c.req.query("discussions_q"); + return c.render("discussions.eta", await getGitHubDiscussions(owner, repo, { q, category })); + }); + + // GitHub puts its own actions (new, categories, ...) where a number goes, so + // anything that isn't one is not a discussion of ours to render. + app.get("/:owner/:repo/discussions/:number", async (c) => { + const { owner, repo, number } = c.req.param(); + if (!/^\d+$/.test(number)) { + return redirectToGitHub(c); + } + return c.render("discussion.eta", await getGitHubDiscussion(owner, repo, Number(number))); + }); + app.get("/:owner/:repo/search", async (c) => { const { owner, repo } = c.req.param(); const q = c.req.query("q") ?? ""; @@ -227,6 +251,8 @@ export function createApp(eta: Eta) { return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(q)}`); } else if (type === "pullrequests") { return c.redirect(`/${owner}/${repo}/pulls?q=${encodeURIComponent(q)}`); + } else if (type === "discussions") { + return c.redirect(`/${owner}/${repo}/discussions?discussions_q=${encodeURIComponent(q)}`); } // Redirect unhandled search types (like code, which requires sign in anyway). -- cgit v1.3.1