From 46d49e74acc85b5e30d3df77631f28ece26b77f4 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Fri, 9 Jan 2026 17:03:47 +0000 Subject: [PATCH 01/17] feat: implement fetch for github sponsors sponsorhip --- .../components/Common/Supporters/index.tsx | 2 +- .../next-data/generators/supportersData.mjs | 160 +++++++++++++++++- apps/site/next.constants.mjs | 5 + apps/site/types/supporters.ts | 1 + 4 files changed, 165 insertions(+), 3 deletions(-) diff --git a/apps/site/components/Common/Supporters/index.tsx b/apps/site/components/Common/Supporters/index.tsx index 06dddf7f23b44..fa894d2ac6c28 100644 --- a/apps/site/components/Common/Supporters/index.tsx +++ b/apps/site/components/Common/Supporters/index.tsx @@ -4,7 +4,7 @@ import type { Supporter } from '#site/types'; import type { FC } from 'react'; type SupportersListProps = { - supporters: Array>; + supporters: Array>; }; const SupportersList: FC = ({ supporters }) => ( diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index fc77c4b383e8b..cb465316f76d1 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -1,4 +1,8 @@ -import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs'; +import { + OPENCOLLECTIVE_MEMBERS_URL, + GITHUB_GRAPHQL_URL, + GITHUB_API_KEY, +} from '#site/next.constants.mjs'; import { fetchWithRetry } from '#site/util/fetch'; /** @@ -26,4 +30,156 @@ 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 of supporters + */ +async function fetchGithubSponsorsData() { + if (!GITHUB_API_KEY) { + return []; + } + + const sponsors = []; + + // Fetch sponsorship pages + let cursor = null; + + while (true) { + const query = sponsorshipsQuery(cursor); + const data = await graphql(query); + + if (data.errors) { + throw new Error(JSON.stringify(data.errors)); + } + + const nodeRes = data.data.user?.sponsorshipsAsMaintainer; + if (!nodeRes) { + break; + } + + const { nodes, pageInfo } = nodeRes; + const mapped = nodes.map(n => { + const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names + return { + id: s?.id || null, + login: s?.login || null, + name: s?.name || s?.login || null, + avatar: s?.avatarUrl || null, + url: s?.websiteUrl || s?.url || null, + }; + }); + + sponsors.push(...mapped); + + if (!pageInfo.hasNextPage) { + break; + } + + cursor = pageInfo.endCursor; + } + return sponsors; +} + +function sponsorshipsQuery(cursor = null) { + return ` + query { + organization(login: "nodejs") { + sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}") { + 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 + } + } + } + }`; +} + +// function donationsQuery(cursor = null) { +// return ` +// query { +// organization(login: "nodejs") { +// sponsorsActivities (first: 100, includePrivate: false, after: "${cursor}") { +// nodes { +// id +// sponsor { +// ...on User { +// id: databaseId, +// name, +// login, +// avatarUrl, +// url, +// websiteUrl +// } +// ...on Organization { +// id: databaseId, +// name, +// login, +// avatarUrl, +// url, +// websiteUrl +// } +// }, +// timestamp +// } +// } +// } +// }`; +// } + +const graphql = async (query, variables = {}) => { + const res = await fetch(GITHUB_GRAPHQL_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${GITHUB_API_KEY}`, + }, + body: JSON.stringify({ query, variables }), + }); + + 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 of supporters + */ +async function sponsorsData() { + const sponsors = await Promise.all([ + fetchGithubSponsorsData(), + fetchOpenCollectiveData(), + ]); + return sponsors.flat(); +} + +export default sponsorsData; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index c90c61711b7c6..59e3c2807b30a 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -219,3 +219,8 @@ export const VULNERABILITIES_URL = */ export const OPENCOLLECTIVE_MEMBERS_URL = 'https://opencollective.com/nodejs/members/all.json'; + +/** + * The location of Github Graphql API + */ +export const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql'; diff --git a/apps/site/types/supporters.ts b/apps/site/types/supporters.ts index 5da04e07c50ca..b0f382b4da6a3 100644 --- a/apps/site/types/supporters.ts +++ b/apps/site/types/supporters.ts @@ -7,3 +7,4 @@ export type Supporter = { }; export type OpenCollectiveSupporter = Supporter<'opencollective'>; +export type GithubSponsorSupporter = Supporter<'github'>; From 00e81c5559cc0685b1c6ccffc3b46addcec0efa4 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 8 Feb 2026 03:19:02 +0000 Subject: [PATCH 02/17] fixup --- apps/site/next-data/generators/supportersData.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index cb465316f76d1..1155932fb2785 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -54,7 +54,7 @@ async function fetchGithubSponsorsData() { throw new Error(JSON.stringify(data.errors)); } - const nodeRes = data.data.user?.sponsorshipsAsMaintainer; + const nodeRes = data.data.organization?.sponsorshipsAsMaintainer; if (!nodeRes) { break; } @@ -79,6 +79,7 @@ async function fetchGithubSponsorsData() { cursor = pageInfo.endCursor; } + return sponsors; } From 309a8bb97d77e4e7bbfd5db713b1ab18edcd80d4 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 8 Feb 2026 03:51:09 +0000 Subject: [PATCH 03/17] feat: implement donations queary and fix links Signed-off-by: Sebastian Beltran --- .../components/Common/Supporters/index.tsx | 9 +- .../next-data/generators/supportersData.mjs | 99 ++++++++++++------- 2 files changed, 65 insertions(+), 43 deletions(-) diff --git a/apps/site/components/Common/Supporters/index.tsx b/apps/site/components/Common/Supporters/index.tsx index fa894d2ac6c28..edc86afe8ada8 100644 --- a/apps/site/components/Common/Supporters/index.tsx +++ b/apps/site/components/Common/Supporters/index.tsx @@ -9,13 +9,8 @@ type SupportersListProps = { const SupportersList: FC = ({ supporters }) => (
- {supporters.map(({ name, image, profile }, i) => ( - + {supporters.map(({ name, image, url }, i) => ( + ))}
); diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index 1155932fb2785..e966327a50082 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -19,11 +19,10 @@ 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, - profile, + url: profile, source: 'opencollective', })); @@ -66,8 +65,9 @@ async function fetchGithubSponsorsData() { id: s?.id || null, login: s?.login || null, name: s?.name || s?.login || null, - avatar: s?.avatarUrl || null, - url: s?.websiteUrl || s?.url || null, + image: s?.avatarUrl || null, + url: s?.url || null, + source: 'github', }; }); @@ -80,6 +80,28 @@ async function fetchGithubSponsorsData() { cursor = pageInfo.endCursor; } + const query = donationsQuery(); + const data = await graphql(query); + + if (data.errors) { + throw new Error(JSON.stringify(data.errors)); + } + + const nodeRes = data.data.organization?.sponsorsActivities; + + const { nodes } = nodeRes; + const mapped = nodes.map(n => { + const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names + return { + name: s?.name || s?.login || null, + image: s?.avatarUrl || null, + url: s?.url || null, + source: 'github', + }; + }); + + sponsors.push(...mapped); + return sponsors; } @@ -119,37 +141,41 @@ function sponsorshipsQuery(cursor = null) { }`; } -// function donationsQuery(cursor = null) { -// return ` -// query { -// organization(login: "nodejs") { -// sponsorsActivities (first: 100, includePrivate: false, after: "${cursor}") { -// nodes { -// id -// sponsor { -// ...on User { -// id: databaseId, -// name, -// login, -// avatarUrl, -// url, -// websiteUrl -// } -// ...on Organization { -// id: databaseId, -// name, -// login, -// avatarUrl, -// url, -// websiteUrl -// } -// }, -// timestamp -// } -// } -// } -// }`; -// } +function donationsQuery() { + return ` + 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 + } + } + } + } + }`; +} const graphql = async (query, variables = {}) => { const res = await fetch(GITHUB_GRAPHQL_URL, { @@ -180,6 +206,7 @@ async function sponsorsData() { fetchGithubSponsorsData(), fetchOpenCollectiveData(), ]); + return sponsors.flat(); } From 70c5076aed6f46d6b9ffc7039ad2a02e886af8ba Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 8 Feb 2026 04:00:00 +0000 Subject: [PATCH 04/17] feat: update text of supporters --- apps/site/next-data/generators/supportersData.mjs | 2 -- apps/site/pages/en/about/partners.mdx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index e966327a50082..e262214a26006 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -62,8 +62,6 @@ async function fetchGithubSponsorsData() { const mapped = nodes.map(n => { const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names return { - id: s?.id || null, - login: s?.login || null, name: s?.name || s?.login || null, image: s?.avatarUrl || null, url: s?.url || null, diff --git a/apps/site/pages/en/about/partners.mdx b/apps/site/pages/en/about/partners.mdx index 7fd69d685fdfc..f2d6421d3af23 100644 --- a/apps/site/pages/en/about/partners.mdx +++ b/apps/site/pages/en/about/partners.mdx @@ -23,7 +23,7 @@ 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. +[OpenCollective](https://opencollective.com/nodejs) and [GitHub Sponsors](https://github.com/sponsors/nodejs) of the Node.js project. From 21655fab244312ec6faf9315c61e2febfbb890b7 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 8 Feb 2026 04:10:09 +0000 Subject: [PATCH 05/17] feat: include past sponsors Signed-off-by: Sebastian Beltran --- apps/site/next-data/generators/supportersData.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index e262214a26006..5d95763b21768 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -107,7 +107,7 @@ function sponsorshipsQuery(cursor = null) { return ` query { organization(login: "nodejs") { - sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}") { + sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}", activeOnly: false) { nodes { sponsor: sponsorEntity { ...on User { From 448e37ed36eaa68d21e777d673ba9edd745cc2b3 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 8 Feb 2026 04:21:06 +0000 Subject: [PATCH 06/17] feat: sort supporters Signed-off-by: Sebastian Beltran --- apps/site/next-data/generators/supportersData.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index 5d95763b21768..b0fa54e12dd16 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -3,6 +3,7 @@ import { GITHUB_GRAPHQL_URL, GITHUB_API_KEY, } from '#site/next.constants.mjs'; +import { shuffle } from '#site/util/array'; import { fetchWithRetry } from '#site/util/fetch'; /** @@ -200,12 +201,17 @@ const graphql = async (query, variables = {}) => { * @returns {Promise>} Array of supporters */ async function sponsorsData() { + const seconds = 300; // Change every 5 minutes + const seed = Math.floor(Date.now() / (seconds * 1000)); + const sponsors = await Promise.all([ fetchGithubSponsorsData(), fetchOpenCollectiveData(), ]); - return sponsors.flat(); + const shuffled = await shuffle(sponsors.flat(), seed); + + return shuffled; } export default sponsorsData; From 0ad4e41eeccc061f54643cc9e2d429eaa4cbf6da Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 7 Feb 2026 23:28:21 -0500 Subject: [PATCH 07/17] Update apps/site/next-data/generators/supportersData.mjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sebastian Beltran --- apps/site/next-data/generators/supportersData.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index b0fa54e12dd16..d601b931f9102 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -87,6 +87,9 @@ async function fetchGithubSponsorsData() { } const nodeRes = data.data.organization?.sponsorsActivities; + if (!nodeRes) { + return sponsors; + } const { nodes } = nodeRes; const mapped = nodes.map(n => { From 6da91ff4ce8c7d6c850da96b9ab7541c0ca9c429 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 7 Feb 2026 23:39:56 -0500 Subject: [PATCH 08/17] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sebastian Beltran --- apps/site/next.constants.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index 59e3c2807b30a..d05678c5f5a93 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -221,6 +221,6 @@ export const OPENCOLLECTIVE_MEMBERS_URL = 'https://opencollective.com/nodejs/members/all.json'; /** - * The location of Github Graphql API + * The location of the GitHub GraphQL API */ export const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql'; From 29829cef910de436cef8123729807bc88d59ab9e Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 18:44:00 +0000 Subject: [PATCH 09/17] fixup! --- apps/site/next-data/generators/supportersData.mjs | 6 +++--- apps/site/next.constants.mjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index b69b60c3cfa90..99061deeba7ad 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -1,7 +1,7 @@ import { OPENCOLLECTIVE_MEMBERS_URL, GITHUB_GRAPHQL_URL, - GITHUB_API_KEY, + GITHUB_READ_API_KEY, } from '#site/next.constants.mjs'; import { fetchWithRetry } from '#site/next.fetch.mjs'; import { shuffle } from '#site/util/array'; @@ -41,7 +41,7 @@ async function fetchOpenCollectiveData() { * @returns {Promise>} Array of supporters */ async function fetchGithubSponsorsData() { - if (!GITHUB_API_KEY) { + if (!GITHUB_READ_API_KEY) { return []; } @@ -188,7 +188,7 @@ const graphql = async (query, variables = {}) => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${GITHUB_API_KEY}`, + Authorization: `Bearer ${GITHUB_READ_API_KEY}`, }, body: JSON.stringify({ query, variables }), }); diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index 4ccb5c8c6e923..7bb3fdc29bcde 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -180,7 +180,7 @@ export const ORAMA_CLOUD_PROJECT_ID = * * 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. From f419e15fde4058ff7249b9fdbda1b93e2f57d9d5 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:01:17 +0000 Subject: [PATCH 10/17] fix: update supporters data mapping to include source and adjust URL handling --- apps/site/components/Common/Supporters/index.tsx | 4 ++-- apps/site/next-data/generators/supportersData.mjs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/site/components/Common/Supporters/index.tsx b/apps/site/components/Common/Supporters/index.tsx index 9037dcc555af7..2797e94241ed9 100644 --- a/apps/site/components/Common/Supporters/index.tsx +++ b/apps/site/components/Common/Supporters/index.tsx @@ -11,12 +11,12 @@ type SupportersListProps = { const SupportersList: FC = ({ supporters }) => (
- {supporters.map(({ name, image, url }) => ( + {supporters.map(({ name, image, source, url }) => ( ))} diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index 99061deeba7ad..6bae880903b82 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -20,12 +20,11 @@ async function fetchOpenCollectiveData() { const members = payload .filter(({ role, isActive }) => role === 'BACKER' && isActive) .sort((a, b) => b.totalAmountDonated - a.totalAmountDonated) - .map(({ name, image, profile, website }) => ({ + .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', From c493a8d697bc4c93911acbf4cfb185ce920b190a Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:07:56 +0000 Subject: [PATCH 11/17] refactor: consolidate GraphQL queries for sponsorships and donations in supportersData.mjs --- .../next-data/generators/supportersData.mjs | 153 +++++++++--------- 1 file changed, 77 insertions(+), 76 deletions(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index 6bae880903b82..f251516a70de9 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -6,6 +6,81 @@ import { 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, * and maps it to the Supporters type. @@ -50,8 +125,7 @@ async function fetchGithubSponsorsData() { let cursor = null; while (true) { - const query = sponsorshipsQuery(cursor); - const data = await graphql(query); + const data = await graphql(SPONSORSHIPS_QUERY, { cursor }); if (data.errors) { throw new Error(JSON.stringify(data.errors)); @@ -82,8 +156,7 @@ async function fetchGithubSponsorsData() { cursor = pageInfo.endCursor; } - const query = donationsQuery(); - const data = await graphql(query); + const data = await graphql(DONATIONS_QUERY); if (data.errors) { throw new Error(JSON.stringify(data.errors)); @@ -110,78 +183,6 @@ async function fetchGithubSponsorsData() { return sponsors; } -function sponsorshipsQuery(cursor = null) { - return ` - query { - 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 - } - } - } - }`; -} - -function donationsQuery() { - return ` - 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 - } - } - } - } - }`; -} - const graphql = async (query, variables = {}) => { const res = await fetch(GITHUB_GRAPHQL_URL, { method: 'POST', From 6f5435bfd9046ec316f1e26340685ade0fb31493 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:10:26 +0000 Subject: [PATCH 12/17] refactor: streamline fetching of GitHub sponsors by combining sponsorships and donations queries --- .../next-data/generators/supportersData.mjs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index f251516a70de9..a03f7f19d05c5 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -119,9 +119,16 @@ async function fetchGithubSponsorsData() { return []; } - const sponsors = []; + const [sponsorships, donations] = await Promise.all([ + fetchSponsorshipsQuery(), + fetchDonationsQuery(), + ]); + + return [...sponsorships, ...donations]; +} - // Fetch sponsorship pages +async function fetchSponsorshipsQuery() { + const sponsors = []; let cursor = null; while (true) { @@ -156,6 +163,10 @@ async function fetchGithubSponsorsData() { cursor = pageInfo.endCursor; } + return sponsors; +} + +async function fetchDonationsQuery() { const data = await graphql(DONATIONS_QUERY); if (data.errors) { @@ -164,11 +175,11 @@ async function fetchGithubSponsorsData() { const nodeRes = data.data.organization?.sponsorsActivities; if (!nodeRes) { - return sponsors; + return []; } const { nodes } = nodeRes; - const mapped = nodes.map(n => { + return nodes.map(n => { const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names return { name: s?.name || s?.login || null, @@ -177,10 +188,6 @@ async function fetchGithubSponsorsData() { source: 'github', }; }); - - sponsors.push(...mapped); - - return sponsors; } const graphql = async (query, variables = {}) => { From 536b553696c7ad03118462751e75c7df16f3ef96 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:13:20 +0000 Subject: [PATCH 13/17] test: add end-to-end test for partners page to verify no 500 error --- apps/site/tests/e2e/supporters-page.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/site/tests/e2e/supporters-page.spec.ts diff --git a/apps/site/tests/e2e/supporters-page.spec.ts b/apps/site/tests/e2e/supporters-page.spec.ts new file mode 100644 index 0000000000000..2f16a68031791 --- /dev/null +++ b/apps/site/tests/e2e/supporters-page.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Supporters page', () => { + test('should not return a 500 error for partners page', async ({ page }) => { + const response = await page.goto('/en/about/partners'); + + expect(response).not.toBeNull(); + expect(response?.status()).not.toBe(500); + + await expect( + page.getByRole('heading', { name: 'Partners & Supporters', level: 1 }) + ).toBeVisible(); + }); +}); From 7cb53c49f1b3a96098c64cd65bd3cb8bfa72218a Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:17:43 +0000 Subject: [PATCH 14/17] refactor: simplify cursor handling and clean up sponsor field mapping in GraphQL queries --- apps/site/next-data/generators/supportersData.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index a03f7f19d05c5..3d27dba3a9aa3 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -132,7 +132,10 @@ async function fetchSponsorshipsQuery() { let cursor = null; while (true) { - const data = await graphql(SPONSORSHIPS_QUERY, { cursor }); + const data = await graphql( + SPONSORSHIPS_QUERY, + cursor ? { cursor } : undefined + ); if (data.errors) { throw new Error(JSON.stringify(data.errors)); @@ -145,7 +148,7 @@ async function fetchSponsorshipsQuery() { const { nodes, pageInfo } = nodeRes; const mapped = nodes.map(n => { - const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names + const s = n.sponsor || n.sponsorEntity; // support different field names return { name: s?.name || s?.login || null, image: s?.avatarUrl || null, @@ -180,7 +183,7 @@ async function fetchDonationsQuery() { const { nodes } = nodeRes; return nodes.map(n => { - const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names + const s = n.sponsor || n.sponsorEntity; // support different field names return { name: s?.name || s?.login || null, image: s?.avatarUrl || null, @@ -191,7 +194,7 @@ async function fetchDonationsQuery() { } const graphql = async (query, variables = {}) => { - const res = await fetch(GITHUB_GRAPHQL_URL, { + const res = await fetchWithRetry(GITHUB_GRAPHQL_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', From 492c9ed79c06913eb1b538571ee6717d1822d09d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:22:10 +0000 Subject: [PATCH 15/17] fix: correct wording in supporters section for clarity --- apps/site/pages/en/about/partners.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/pages/en/about/partners.mdx b/apps/site/pages/en/about/partners.mdx index f2d6421d3af23..5498ebbdab72b 100644 --- a/apps/site/pages/en/about/partners.mdx +++ b/apps/site/pages/en/about/partners.mdx @@ -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) and [GitHub Sponsors](https://github.com/sponsors/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). From 512ba913909888fc42ef1cd17919e454e6be6c0d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:42:02 +0000 Subject: [PATCH 16/17] chore: standardize naming for GitHub sponsor supporter and update API key handling --- .../playwright-cloudflare-open-next.yml | 1 + .../next-data/generators/supportersData.mjs | 17 +++++++++++++---- apps/site/types/supporters.ts | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index 8a42b4675fcd8..21534da568bf3 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -56,6 +56,7 @@ jobs: env: PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW: true PLAYWRIGHT_BASE_URL: http://127.0.0.1:8787 + GITHUB_READ_API_KEY: ${{ secrets.GITHUB_READ_API_KEY }} - name: Upload Playwright test results if: always() diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index 3d27dba3a9aa3..371521166899a 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -112,7 +112,7 @@ async function fetchOpenCollectiveData() { * Fetches supporters data from Github API, filters active backers, * and maps it to the Supporters type. * - * @returns {Promise>} Array of supporters + * @returns {Promise>} Array of supporters */ async function fetchGithubSponsorsData() { if (!GITHUB_READ_API_KEY) { @@ -215,18 +215,27 @@ const graphql = async (query, variables = {}) => { * Fetches supporters data from Open Collective API and GitHub Sponsors, filters active backers, * and maps it to the Supporters type. * - * @returns {Promise>} Array of supporters + * @returns {Promise>} Array of supporters */ async function sponsorsData() { const seconds = 300; // Change every 5 minutes const seed = Math.floor(Date.now() / (seconds * 1000)); - const sponsors = await Promise.all([ + const sponsorsResults = await Promise.allSettled([ fetchGithubSponsorsData(), fetchOpenCollectiveData(), ]); - const shuffled = await shuffle(sponsors.flat(), seed); + 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; } diff --git a/apps/site/types/supporters.ts b/apps/site/types/supporters.ts index 301b9d4edcf22..b97dddf5089d1 100644 --- a/apps/site/types/supporters.ts +++ b/apps/site/types/supporters.ts @@ -7,4 +7,4 @@ export type Supporter = { }; export type OpenCollectiveSupporter = Supporter<'opencollective'>; -export type GithubSponsorSupporter = Supporter<'github'>; +export type GitHubSponsorSupporter = Supporter<'github'>; From dfdf2ab061d4b84f82189831435fdfdc737149fd Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 12 Apr 2026 19:49:06 +0000 Subject: [PATCH 17/17] remove test --- .../workflows/playwright-cloudflare-open-next.yml | 1 - apps/site/tests/e2e/supporters-page.spec.ts | 14 -------------- 2 files changed, 15 deletions(-) delete mode 100644 apps/site/tests/e2e/supporters-page.spec.ts diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml index 21534da568bf3..8a42b4675fcd8 100644 --- a/.github/workflows/playwright-cloudflare-open-next.yml +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -56,7 +56,6 @@ jobs: env: PLAYWRIGHT_RUN_CLOUDFLARE_PREVIEW: true PLAYWRIGHT_BASE_URL: http://127.0.0.1:8787 - GITHUB_READ_API_KEY: ${{ secrets.GITHUB_READ_API_KEY }} - name: Upload Playwright test results if: always() diff --git a/apps/site/tests/e2e/supporters-page.spec.ts b/apps/site/tests/e2e/supporters-page.spec.ts deleted file mode 100644 index 2f16a68031791..0000000000000 --- a/apps/site/tests/e2e/supporters-page.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Supporters page', () => { - test('should not return a 500 error for partners page', async ({ page }) => { - const response = await page.goto('/en/about/partners'); - - expect(response).not.toBeNull(); - expect(response?.status()).not.toBe(500); - - await expect( - page.getByRole('heading', { name: 'Partners & Supporters', level: 1 }) - ).toBeVisible(); - }); -});