From cd35e595decf99ec85c616710d5ae4c706a83f17 Mon Sep 17 00:00:00 2001 From: castrojo Date: Tue, 8 Sep 2026 15:23:14 +0000 Subject: [PATCH] fix(contributors): prevent visitor-side GitHub requests when contributor data is unavailable When fetch-contributors.js fails to fetch commits for every file (e.g. no GitHub token during build, or rate limited), it previously wrote an empty object to file-contributors.json. PageContributors.tsx then treated every missing key as a per-page cache miss and fell back to an unauthenticated GitHub API request from each visitor's browser on every page footer. Emit a sentinel '__contributors_unavailable__' key when the build-time fetch produces zero successful results, and have PageContributors.tsx detect that sentinel and skip its client-side API fallback entirely instead of hammering the GitHub API per page. Signed-off-by: castrojo --- scripts/fetch-contributors.js | 19 ++++++++++++++++--- src/components/PageContributors.tsx | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/scripts/fetch-contributors.js b/scripts/fetch-contributors.js index 6bd7aaf6e..d18862127 100644 --- a/scripts/fetch-contributors.js +++ b/scripts/fetch-contributors.js @@ -10,6 +10,12 @@ const OUTPUT_FILE = path.join(OUTPUT_DIR, "file-contributors.json"); const DOCS_DIR = path.join(__dirname, "..", "docs"); const BLOG_DIR = path.join(__dirname, "..", "blog"); +// Sentinel key written when a complete fetch run yields zero successful +// results. Consumers (e.g. PageContributors.tsx) use this to detect a +// globally unavailable dataset and suppress per-page client-side API +// fallback requests, instead of treating every missing key as a cache miss. +const UNAVAILABLE_KEY = "__contributors_unavailable__"; + // Cache configuration const CACHE_MAX_AGE_HOURS = 24; @@ -143,19 +149,25 @@ async function fetchAllContributors() { }, ); - const contributorsData = Object.fromEntries(resultsMap); const successCount = resultsMap.size; console.log( `\nSuccessfully fetched contributors for ${successCount}/${allFiles.length} files`, ); - // Don't fail build if no contributors fetched - component will gracefully handle empty data - if (successCount === 0) { + // Don't fail build if no contributors fetched - component will gracefully handle empty data. + // When nothing succeeded (e.g. no token / rate limited / offline), emit a sentinel + // payload instead of an empty object so the client knows the whole dataset is + // unavailable and can skip its per-page GitHub API fallback requests. + let contributorsData; + if (allFiles.length > 0 && successCount === 0) { console.warn( "\n⚠️ No contributors fetched! Contributors will not be displayed.", ); console.warn(" Please set a GitHub token and try again."); + contributorsData = { [UNAVAILABLE_KEY]: true }; + } else { + contributorsData = Object.fromEntries(resultsMap); } // Ensure output directory exists @@ -183,4 +195,5 @@ if (require.main === module) { module.exports = { getAllMarkdownFiles, isBotAccount, + UNAVAILABLE_KEY, }; diff --git a/src/components/PageContributors.tsx b/src/components/PageContributors.tsx index a44357134..a4bf1ce0b 100644 --- a/src/components/PageContributors.tsx +++ b/src/components/PageContributors.tsx @@ -19,6 +19,15 @@ interface PageContributorsProps { const CACHE_KEY_PREFIX = "file_contributors_"; const CACHE_DURATION = 30 * 24 * 60 * 60 * 1000; // 30 days +// Must match UNAVAILABLE_KEY in scripts/fetch-contributors.js. When the +// build-time fetch produced zero successful results, the dataset carries +// this sentinel instead of per-file entries, signalling that every visitor +// browser should skip its GitHub API fallback rather than hammering the +// unauthenticated API on every page load. +const UNAVAILABLE_KEY = "__contributors_unavailable__"; +const isDatasetGloballyUnavailable = + (contributorsData as Record)[UNAVAILABLE_KEY] === true; + // Global request queue to prevent rate limiting class RequestQueue { private queue: Array<() => Promise> = []; @@ -166,6 +175,14 @@ const PageContributors: React.FC = ({ filePath }) => { } } + // If the build-time dataset is globally unavailable (e.g. the fetch + // script had no token or was rate-limited during the build), don't fall + // back to per-page client-side API requests - just show nothing. + if (isDatasetGloballyUnavailable) { + setLoading(false); + return; + } + // Finally, fetch from GitHub API as fallback fetchFileContributors(filePath) .then((data) => {