Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 16 additions & 3 deletions scripts/fetch-contributors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -183,4 +195,5 @@ if (require.main === module) {
module.exports = {
getAllMarkdownFiles,
isBotAccount,
UNAVAILABLE_KEY,
};
17 changes: 17 additions & 0 deletions src/components/PageContributors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)[UNAVAILABLE_KEY] === true;

// Global request queue to prevent rate limiting
class RequestQueue {
private queue: Array<() => Promise<void>> = [];
Expand Down Expand Up @@ -166,6 +175,14 @@ const PageContributors: React.FC<PageContributorsProps> = ({ 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) => {
Expand Down