Skip to content

Commit 23c4c6b

Browse files
fix(github): proxy discussions server-side
Keep discussions live in production without exposing a client token.\n\nRoute discussion reads through a server-side API endpoint, preserve\nlegacy env compatibility during migration, and document the required\nenvironment variables.
1 parent 0190eea commit 23c4c6b

11 files changed

Lines changed: 274 additions & 402 deletions

File tree

.env.example

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
# GitHub API Configuration (Optional)
2-
# To avoid rate limits, you can add a GitHub Personal Access Token
3-
# Create one at: https://github.com/settings/tokens
4-
# No special permissions needed for public repositories
1+
# GitHub API Configuration
2+
# Used by the server-side /api/github-discussions endpoint.
3+
# Prefer GITHUB_TOKEN for new setups. DOCUSAURUS_GIT_TOKEN is still supported as a legacy fallback.
4+
# Create a Classic PAT at: https://github.com/settings/tokens
5+
# Recommended scopes: public_repo, read:org, read:discussion
56
GITHUB_TOKEN=your_github_token_here
67

7-
# GitHub token used by Docusaurus for dynamic features (discussions, stats, leaderboard)
8-
# This must be set for the discussions section to fetch live data from GitHub
9-
# Create a Classic PAT with read:discussion scope at https://github.com/settings/tokens
8+
# Legacy fallback for existing deployments. Optional if GITHUB_TOKEN is already set.
109
DOCUSAURUS_GIT_TOKEN=your_github_token_here
1110

1211
# Shopify Configuration (for Merch Store)

api/github-discussions.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
const ORG_NAME = "recodehive";
2+
const DISCUSSIONS_REPO = "recode-website";
3+
const DEFAULT_LIMIT = 20;
4+
const UNAVAILABLE_MESSAGE =
5+
"GitHub Discussions are available only when a server-side GITHUB_TOKEN or DOCUSAURUS_GIT_TOKEN is configured.";
6+
7+
function getToken() {
8+
return process.env.GITHUB_TOKEN?.trim() || process.env.DOCUSAURUS_GIT_TOKEN?.trim() || "";
9+
}
10+
11+
function parseLimit(limitParam) {
12+
const parsed = Number.parseInt(limitParam, 10);
13+
if (!Number.isFinite(parsed) || parsed <= 0) {
14+
return DEFAULT_LIMIT;
15+
}
16+
17+
return Math.min(parsed, 50);
18+
}
19+
20+
function mapDiscussion(discussion) {
21+
return {
22+
id: discussion.id,
23+
title: discussion.title || "Untitled discussion",
24+
body: discussion.body || "",
25+
author: {
26+
login: discussion.author?.login || "Unknown",
27+
avatar_url: discussion.author?.avatarUrl || "",
28+
html_url: discussion.author?.url || "",
29+
},
30+
category: {
31+
name: discussion.category?.name || "General",
32+
emoji: discussion.category?.emoji || "",
33+
},
34+
created_at: discussion.createdAt,
35+
updated_at: discussion.updatedAt,
36+
comments: discussion.comments?.totalCount || 0,
37+
reactions: {
38+
total_count: discussion.reactions?.totalCount || 0,
39+
},
40+
html_url: discussion.url,
41+
labels:
42+
discussion.labels?.nodes?.map((label) => ({
43+
name: label.name,
44+
color: label.color,
45+
})) || [],
46+
};
47+
}
48+
49+
async function fetchGitHubDiscussions(token, limit) {
50+
const query = `
51+
query GetDiscussions($owner: String!, $name: String!, $first: Int!) {
52+
repository(owner: $owner, name: $name) {
53+
discussions(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) {
54+
totalCount
55+
nodes {
56+
id
57+
title
58+
body
59+
createdAt
60+
updatedAt
61+
url
62+
author {
63+
login
64+
avatarUrl
65+
url
66+
}
67+
category {
68+
name
69+
emoji
70+
}
71+
comments {
72+
totalCount
73+
}
74+
reactions {
75+
totalCount
76+
}
77+
labels(first: 10) {
78+
nodes {
79+
name
80+
color
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
`;
88+
89+
const response = await fetch("https://api.github.com/graphql", {
90+
method: "POST",
91+
headers: {
92+
Accept: "application/vnd.github.v3+json",
93+
Authorization: `Bearer ${token}`,
94+
"Content-Type": "application/json",
95+
},
96+
body: JSON.stringify({
97+
query,
98+
variables: {
99+
owner: ORG_NAME,
100+
name: DISCUSSIONS_REPO,
101+
first: limit,
102+
},
103+
}),
104+
});
105+
106+
if (!response.ok) {
107+
throw new Error(`GitHub discussions request failed: ${response.status}`);
108+
}
109+
110+
const payload = await response.json();
111+
112+
if (payload.errors?.length) {
113+
const message = payload.errors.map((error) => error.message).join(", ");
114+
throw new Error(message || "GitHub discussions GraphQL query failed");
115+
}
116+
117+
const discussions = payload.data?.repository?.discussions;
118+
119+
return {
120+
available: true,
121+
message: null,
122+
totalCount: discussions?.totalCount ?? 0,
123+
discussions: (discussions?.nodes || []).map(mapDiscussion),
124+
fetchedAt: new Date().toISOString(),
125+
};
126+
}
127+
128+
export default async function handler(req, res) {
129+
const token = getToken();
130+
131+
res.setHeader(
132+
"Cache-Control",
133+
"public, s-maxage=300, stale-while-revalidate=600",
134+
);
135+
136+
if (!token) {
137+
res.status(503).json({
138+
available: false,
139+
message: UNAVAILABLE_MESSAGE,
140+
totalCount: null,
141+
discussions: [],
142+
fetchedAt: null,
143+
});
144+
return;
145+
}
146+
147+
try {
148+
const limit = parseLimit(req.query.limit);
149+
const data = await fetchGitHubDiscussions(token, limit);
150+
res.status(200).json(data);
151+
} catch (error) {
152+
res.status(502).json({
153+
available: false,
154+
message:
155+
error instanceof Error
156+
? error.message
157+
: "Failed to fetch GitHub discussions.",
158+
totalCount: null,
159+
discussions: [],
160+
fetchedAt: null,
161+
});
162+
}
163+
}

docusaurus.config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const config: Config = {
1919
projectName: "recode-website",
2020

2121
onBrokenLinks: "throw",
22-
// onBrokenMarkdownLinks moved to markdown.hooks
2322

2423
// Google Analytics and Theme Scripts
2524
scripts: [
@@ -271,8 +270,6 @@ const config: Config = {
271270
},
272271
},
273272

274-
// Migrated legacy setting to markdown.hooks.onBrokenMarkdownLinks
275-
276273
themes: ["@docusaurus/theme-mermaid"],
277274

278275
plugins: [

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import tsPlugin from "@typescript-eslint/eslint-plugin";
44
import reactPlugin from "eslint-plugin-react";
55

66
export default [
7+
{
8+
ignores: ["node_modules/", "build/", ".docusaurus/", "static/", "dist/"],
9+
},
710
{
811
files: ["**/*.{ts,tsx}"],
912
ignores: ["node_modules/", "build/", ".docusaurus/", "static/", "dist/"],

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"write-translations": "docusaurus write-translations",
1515
"write-heading-ids": "docusaurus write-heading-ids",
1616
"typecheck": "tsc",
17-
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
18-
"lint:fix": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix",
17+
"lint": "eslint .",
18+
"lint:fix": "eslint . --fix",
1919
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css}\"",
2020
"format:check": "prettier --check .",
2121
"prepare": "husky"

src/lib/statsProvider.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, {
77
useState,
88
type ReactNode,
99
} from "react";
10-
import { githubService, type GitHubOrgStats } from "../services/githubService";
10+
import { githubService } from "../services/githubService";
1111

1212
// Time filter types
1313
export type TimeFilter = "week" | "month" | "year" | "all";
@@ -435,17 +435,18 @@ export function CommunityStatsProvider({
435435
};
436436

437437
// Fetch both org stats and repos in parallel
438-
const [orgStats, repos] = await Promise.all([
438+
const [orgStats, repos, discussionsCount] = await Promise.all([
439439
githubService.fetchOrganizationStats(signal),
440440
fetchAllOrgRepos(headers),
441+
githubService.fetchDiscussionsCount(signal),
441442
]);
442443

443444
// Set org stats immediately
444445
setGithubStarCount(orgStats.totalStars);
445446
setGithubContributorsCount(orgStats.totalContributors);
446447
setGithubForksCount(orgStats.totalForks);
447448
setGithubReposCount(orgStats.publicRepositories);
448-
setGithubDiscussionsCount(orgStats.discussionsCount);
449+
setGithubDiscussionsCount(discussionsCount ?? orgStats.discussionsCount);
449450
setLastUpdated(new Date(orgStats.lastUpdated));
450451

451452
// Process leaderboard data with concurrent processing

0 commit comments

Comments
 (0)