Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
46d49e7
feat: implement fetch for github sponsors sponsorhip
bjohansebas Jan 9, 2026
00e81c5
fixup
bjohansebas Feb 8, 2026
309a8bb
feat: implement donations queary and fix links
bjohansebas Feb 8, 2026
70c5076
feat: update text of supporters
bjohansebas Feb 8, 2026
21655fa
feat: include past sponsors
bjohansebas Feb 8, 2026
448e37e
feat: sort supporters
bjohansebas Feb 8, 2026
0ad4e41
Update apps/site/next-data/generators/supportersData.mjs
bjohansebas Feb 8, 2026
6da91ff
Apply suggestion from @Copilot
bjohansebas Feb 8, 2026
fccada1
Merge branch 'main' of github.com:nodejs/nodejs.org into github_sponsors
bjohansebas Apr 12, 2026
29829ce
fixup!
bjohansebas Apr 12, 2026
f419e15
fix: update supporters data mapping to include source and adjust URL …
bjohansebas Apr 12, 2026
c493a8d
refactor: consolidate GraphQL queries for sponsorships and donations …
bjohansebas Apr 12, 2026
6f5435b
refactor: streamline fetching of GitHub sponsors by combining sponsor…
bjohansebas Apr 12, 2026
536b553
test: add end-to-end test for partners page to verify no 500 error
bjohansebas Apr 12, 2026
7cb53c4
refactor: simplify cursor handling and clean up sponsor field mapping…
bjohansebas Apr 12, 2026
492c9ed
fix: correct wording in supporters section for clarity
bjohansebas Apr 12, 2026
512ba91
chore: standardize naming for GitHub sponsor supporter and update API…
bjohansebas Apr 12, 2026
dfdf2ab
remove test
bjohansebas Apr 12, 2026
4d0c924
Merge branch 'main' into github_sponsors
bmuenzenmeyer Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/site/components/Common/Supporters/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import type { Supporter } from '#site/types';
import type { FC } from 'react';

type SupportersListProps = {
supporters: Array<Supporter<'opencollective'>>;
supporters: Array<Supporter<'opencollective' | 'github'>>;
};

const SupportersList: FC<SupportersListProps> = ({ supporters }) => (
<div className="flex max-w-full flex-wrap items-center justify-center gap-1">
{supporters.map(({ name, image, profile }) => (
{supporters.map(({ name, image, source, url }) => (
<Avatar
nickname={name}
fallback={getAcronymFromString(name)}
image={image}
key={name}
url={profile}
key={`${source}:${name}`}
url={url}
Comment thread
cursor[bot] marked this conversation as resolved.
/>
))}
</div>
Expand Down
221 changes: 216 additions & 5 deletions apps/site/next-data/generators/supportersData.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,85 @@
import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs';
import {
OPENCOLLECTIVE_MEMBERS_URL,
GITHUB_GRAPHQL_URL,
GITHUB_READ_API_KEY,
} from '#site/next.constants.mjs';
import { fetchWithRetry } from '#site/next.fetch.mjs';
import { shuffle } from '#site/util/array';

const SPONSORSHIPS_QUERY = `
query ($cursor: String) {
organization(login: "nodejs") {
sponsorshipsAsMaintainer(
first: 100
includePrivate: false
after: $cursor
activeOnly: false
) {
nodes {
sponsor: sponsorEntity {
...on User {
id: databaseId
name
login
avatarUrl
url
websiteUrl
}
...on Organization {
id: databaseId
name
login
avatarUrl
url
websiteUrl
}
}
}
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
}
}
}
`;

const DONATIONS_QUERY = `
query {
organization(login: "nodejs") {
sponsorsActivities(first: 100, includePrivate: false) {
nodes {
id
sponsor {
...on User {
id: databaseId
name
login
avatarUrl
url
websiteUrl
}
...on Organization {
id: databaseId
name
login
avatarUrl
url
websiteUrl
}
}
timestamp
tier: sponsorsTier {
monthlyPriceInDollars
isOneTime
}
}
}
}
}
`;

/**
* Fetches supporters data from Open Collective API, filters active backers,
Expand All @@ -15,12 +95,11 @@ async function fetchOpenCollectiveData() {
const members = payload
.filter(({ role, isActive }) => role === 'BACKER' && isActive)
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
.map(({ name, website, image, profile }) => ({
.map(({ name, image, profile }) => ({
name,
image,
url: website,
// If profile starts with the guest- prefix, it's a non-existing account
profile: profile.startsWith('https://opencollective.com/guest-')
url: profile.startsWith('https://opencollective.com/guest-')
? undefined
: profile,
source: 'opencollective',
Expand All @@ -29,4 +108,136 @@ async function fetchOpenCollectiveData() {
return members;
}

export default fetchOpenCollectiveData;
/**
* Fetches supporters data from Github API, filters active backers,
* and maps it to the Supporters type.
*
* @returns {Promise<Array<import('#site/types/supporters').GitHubSponsorSupporter>>} Array of supporters
*/
async function fetchGithubSponsorsData() {
Comment thread
bjohansebas marked this conversation as resolved.
if (!GITHUB_READ_API_KEY) {
return [];
}

const [sponsorships, donations] = await Promise.all([
fetchSponsorshipsQuery(),
fetchDonationsQuery(),
]);

return [...sponsorships, ...donations];
}

async function fetchSponsorshipsQuery() {
const sponsors = [];
let cursor = null;

while (true) {
const data = await graphql(
SPONSORSHIPS_QUERY,
cursor ? { cursor } : undefined
);

if (data.errors) {
throw new Error(JSON.stringify(data.errors));
}

const nodeRes = data.data.organization?.sponsorshipsAsMaintainer;
if (!nodeRes) {
break;
}

const { nodes, pageInfo } = nodeRes;
const mapped = nodes.map(n => {
const s = n.sponsor || n.sponsorEntity; // support different field names
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphQL alias makes n.sponsorEntity fallback unreachable dead code

Low Severity

The || n.sponsorEntity fallback in both fetchSponsorshipsQuery and fetchDonationsQuery is unreachable. In SPONSORSHIPS_QUERY, the GraphQL alias sponsor: sponsorEntity ensures the response field is always sponsorsponsorEntity never appears in the JSON response. In DONATIONS_QUERY, the field is natively named sponsor. So n.sponsorEntity is always undefined, making the fallback dead code with a misleading comment.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 492c9ed. Configure here.

return {
name: s?.name || s?.login || null,
image: s?.avatarUrl || null,
url: s?.url || null,
source: 'github',
};
Comment thread
bjohansebas marked this conversation as resolved.
});

sponsors.push(...mapped);

if (!pageInfo.hasNextPage) {
break;
}

cursor = pageInfo.endCursor;
}

return sponsors;
}

async function fetchDonationsQuery() {
const data = await graphql(DONATIONS_QUERY);

if (data.errors) {
throw new Error(JSON.stringify(data.errors));
}

const nodeRes = data.data.organization?.sponsorsActivities;
Comment thread
bjohansebas marked this conversation as resolved.
if (!nodeRes) {
return [];
}

const { nodes } = nodeRes;
Comment thread
bjohansebas marked this conversation as resolved.
return nodes.map(n => {
const s = n.sponsor || n.sponsorEntity; // support different field names
return {
name: s?.name || s?.login || null,
image: s?.avatarUrl || null,
url: s?.url || null,
source: 'github',
};
});
Comment thread
bjohansebas marked this conversation as resolved.
}

const graphql = async (query, variables = {}) => {
const res = await fetchWithRetry(GITHUB_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${GITHUB_READ_API_KEY}`,
},
body: JSON.stringify({ query, variables }),
});
Comment thread
bjohansebas marked this conversation as resolved.

if (!res.ok) {
const text = await res.text();
throw new Error(`GitHub API error: ${res.status} ${text}`);
}

return res.json();
};

/**
* Fetches supporters data from Open Collective API and GitHub Sponsors, filters active backers,
* and maps it to the Supporters type.
*
* @returns {Promise<Array<import('#site/types/supporters').OpenCollectiveSupporter | import('#site/types/supporters').GitHubSponsorSupporter>>} Array of supporters
*/
async function sponsorsData() {
const seconds = 300; // Change every 5 minutes
const seed = Math.floor(Date.now() / (seconds * 1000));

const sponsorsResults = await Promise.allSettled([
fetchGithubSponsorsData(),
fetchOpenCollectiveData(),
]);

const sponsors = sponsorsResults.flatMap(result => {
if (result.status === 'fulfilled') {
return result.value;
}

console.error('Supporters data source failed:', result.reason);
return [];
});

const shuffled = await shuffle(sponsors, seed);

return shuffled;
}
Comment on lines +220 to +241
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces substantial new external-fetching logic (GitHub GraphQL pagination, error handling, and merging with OpenCollective) but there are still no generator tests for supportersData.mjs (other generators like releaseData/vulnerabilities have tests). Adding tests that mock fetch to cover pagination, missing token behavior, and de-duping/shape mapping would help prevent regressions.

Copilot uses AI. Check for mistakes.

export default sponsorsData;
7 changes: 6 additions & 1 deletion apps/site/next.constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export const EXTERNAL_LINKS_SITEMAP = [
*
* Note: This has no NEXT_PUBLIC prefix as it should not be exposed to the Browser.
*/
export const GITHUB_API_KEY = process.env.NEXT_GITHUB_API_KEY || '';
export const GITHUB_READ_API_KEY = process.env.NEXT_GITHUB_READ_API_KEY || '';

/**
* The resource we point people to when discussing internationalization efforts.
Expand Down Expand Up @@ -178,6 +178,11 @@ export const VULNERABILITIES_URL =
export const OPENCOLLECTIVE_MEMBERS_URL =
'https://opencollective.com/nodejs/members/all.json';

/**
* The location of the GitHub GraphQL API
*/
export const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql';

/**
* Orama DB URLs for the Learn and API sections of the website
*/
Expand Down
4 changes: 2 additions & 2 deletions apps/site/pages/en/about/partners.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ without we can't test and release new versions of Node.js.

## Supporters

Supporters are individuals and organizations that provide financial support through
[OpenCollective](https://opencollective.com/nodejs) of the Node.js project.
Supporters are individuals and organizations who financially support the Node.js project
through [OpenCollective](https://opencollective.com/nodejs) and [GitHub Sponsors](https://github.com/sponsors/nodejs).

<WithSupporters />

Expand Down
1 change: 1 addition & 0 deletions apps/site/types/supporters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export type Supporter<T extends string> = {
};

export type OpenCollectiveSupporter = Supporter<'opencollective'>;
export type GitHubSponsorSupporter = Supporter<'github'>;
Loading