feat: resolve team avatars to GitHub profiles (#78)#101
Conversation
Resolves #78. Team-card avatars now link to a contributor's GitHub profile (and use their GitHub profile picture) when their commit email can be matched to a GitHub user. The mailto: link remains as the last-resort fallback for contributors whose email isn't tied to any GitHub account. Resolution happens via a single GraphQL query to GitHub's repository.history endpoint. Results are cached to disk (positives and negatives both) so subsequent builds make zero API calls. The walker bails after maxStalePages (5) consecutive pages with no progress, so unresolvable emails don't waste calls on every fresh build. New config options under themeConfig.contributors: - resolveGitHub: 'auto' | true | false (default 'auto') - cachePath: e.g. 'docs/.vitepress/cache/team-github.json' - repo: optional 'owner/name' override (auto-sniffed from git remote) Configured maintainers with an existing github link in their `links` array also benefit: we scrape the username from the link itself (no API call) to populate the new `member.github` field, swap their gravatar avatar for a GitHub avatar, and improve the tooltip. The avatar tooltip now prefers @username over <email> when available, reducing email-address exposure on public team pages.
✅ Deploy Preview for vitepress-theme-default-plus ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: No behavioral difference between
'auto'andtruemodes- Added early short-circuit for 'auto' mode when no token is present, and console warning for 'true' mode to distinguish behaviors.
Or push these changes by commenting:
@cursor push 0e7ef0ad21
Preview (0e7ef0ad21)
diff --git a/node/resolve-github-usernames.js b/node/resolve-github-usernames.js
--- a/node/resolve-github-usernames.js
+++ b/node/resolve-github-usernames.js
@@ -97,6 +97,7 @@
// 5 is forgiving enough that a contributor whose only commits are buried
// a few pages deep (e.g. they switched emails) still gets resolved.
maxStalePages = 5,
+ warnOnMissingToken = false,
debug = Debug('@lando/resolve-github-usernames'), // eslint-disable-line
} = {}) {
// start with whatever's in the cache (a prior run's results). cache values
@@ -118,6 +119,9 @@
// need a token to talk to the API; warn and bail if missing
if (!token) {
+ if (warnOnMissingToken) {
+ console.warn('[vitepress-theme-default-plus] resolveGitHub is set to `true` but no GITHUB_TOKEN or GH_TOKEN environment variable is set; skipping GitHub username resolution');
+ }
debug('no GITHUB_TOKEN/GH_TOKEN env var set; skipping API resolution for %o emails', unresolved.size);
return result;
}
diff --git a/utils/get-contributors.js b/utils/get-contributors.js
--- a/utils/get-contributors.js
+++ b/utils/get-contributors.js
@@ -184,42 +184,49 @@
// a `github` field for tooltips, and seeds a GitHub link in the
// contributor's social-links array when none is configured.
if (resolveGitHub !== false && data.length > 0) {
- let mappings = null;
- const repoCoord = repo && typeof repo === 'object' && repo.owner ? repo : getRepoCoordinate(cwd, {
- override: typeof repo === 'string' ? repo : undefined,
- packageJson,
- debug: debug.extend('repo-coord'),
- });
+ // 'auto' mode: short-circuit early if no token is present, avoiding
+ // unnecessary repo-coordinate lookup and resolver invocation
+ if (resolveGitHub === 'auto' && !token && !process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
+ debug('resolveGitHub is "auto" and no token available; skipping GitHub username resolution');
+ } else {
+ let mappings = null;
+ const repoCoord = repo && typeof repo === 'object' && repo.owner ? repo : getRepoCoordinate(cwd, {
+ override: typeof repo === 'string' ? repo : undefined,
+ packageJson,
+ debug: debug.extend('repo-coord'),
+ });
- if (repoCoord) {
- // only ask the api for emails we don't already have a github link for
- // (configured maintainers contribute their username via the link)
- const emailsToResolve = data
- .filter(c => !c.links?.some(link => link?.icon === 'github'))
- .map(c => c.email)
- .filter(Boolean);
+ if (repoCoord) {
+ // only ask the api for emails we don't already have a github link for
+ // (configured maintainers contribute their username via the link)
+ const emailsToResolve = data
+ .filter(c => !c.links?.some(link => link?.icon === 'github'))
+ .map(c => c.email)
+ .filter(Boolean);
- if (emailsToResolve.length > 0) {
- // resolve relative cache paths against the git root so users can
- // configure something convenient like 'docs/.vitepress/cache/...'
- const resolvedCachePath = cachePath
- ? (isAbsolute(cachePath) ? cachePath : resolve(cwd, cachePath))
- : undefined;
- mappings = await resolveGitHubUsernames(emailsToResolve, {
- repo: repoCoord,
- token,
- cachePath: resolvedCachePath,
- debug: debug.extend('resolve-github'),
- });
- } else {
- debug('all contributors already have github links configured; skipping API resolution');
+ if (emailsToResolve.length > 0) {
+ // resolve relative cache paths against the git root so users can
+ // configure something convenient like 'docs/.vitepress/cache/...'
+ const resolvedCachePath = cachePath
+ ? (isAbsolute(cachePath) ? cachePath : resolve(cwd, cachePath))
+ : undefined;
+ mappings = await resolveGitHubUsernames(emailsToResolve, {
+ repo: repoCoord,
+ token,
+ cachePath: resolvedCachePath,
+ debug: debug.extend('resolve-github'),
+ warnOnMissingToken: resolveGitHub === true,
+ });
+ } else {
+ debug('all contributors already have github links configured; skipping API resolution');
+ }
}
+
+ // always apply — even with no api mappings, this scrapes existing
+ // github links on maintainer entries and uses them to swap avatars
+ // and populate the `github` field
+ applyGitHubLogins(data, mappings);
}
-
- // always apply — even with no api mappings, this scrapes existing
- // github links on maintainer entries and uses them to swap avatars
- // and populate the `github` field
- applyGitHubLogins(data, mappings);
}
// separate maintainers from contribsYou can send follow-ups to the cloud agent here.
…lers
Verified the original commit against the lando/core docs site (which
uses this theme) and surfaced three issues that left most contributors
falling back to mailto:
1. cachePath wasn't a default. The lando v3/v4 presets in
config/landov{3,4}.js define their own contributors block that
overrides the base default, so resolveGitHub:'auto' was kicking in
but cachePath was undefined. Result: cache file never got written,
so per-page transformPageData calls re-ran the resolver fresh
instead of hitting cache.
2. maxPages=10 was too small. lando/core has 223 unique contributors
across ~7000 commits; 1000 commits scanned only resolved ~14 of
them. Bumped maxPages default to 100 (10000 commits) and
maxStalePages to 10. With 100 pages of headroom the walker either
exhausts history (typical case) or bails on a long stretch of
unresolvable emails.
3. Hitting maxPages was poisoning the negative cache. When the walker
cut off early at maxPages, unresolved emails got null'd in the
cache and never retried. Fixed: only write null when the search
actually exhausted (ran off the end of history or hit
maxStalePages). Cut-offs at maxPages leave emails unrecorded so
the next build can pick them up.
Also threaded maxPages and maxStalePages as configurable options on
themeConfig.contributors for users with deeper-than-default histories.
After these fixes, lando/core resolves 190/223 contributors (up from
17/223). Remaining 31 are genuinely unresolvable (deleted accounts,
typos, unconnected emails) and stay as mailto: fallbacks.
When GitHub resolution is enabled (the default), an unresolvable contributor's avatar no longer falls back to a `mailto:` link. The avatar simply isn't a link at all — the existing VPLLink component already handles a missing href by rendering a <span> instead of an <a>, so there's no broken-link rendering. This is a deliberate privacy improvement: once a docs site is using GitHub resolution, the only contributors whose avatars still leak their commit email were the ones we *couldn't* match to a GitHub account anyway, so the mailto: was already low-value. Suppressing it prevents accidental email-harvesting from public team pages. Verified on lando/core: 235 contributors total, 204 now link to GitHub, 31 remain as unlinked gravatar placeholders, ZERO mailto: links anywhere on team.html (down from 31 mailtos with the old fallback behavior). Configurable via themeConfig.contributors.mailtoFallback: - 'auto' (default): off when resolveGitHub is on, on when it's off - true: always keep the legacy mailto fallback - false: never use mailto fallback The 'auto' value is resolved to a boolean once in defineConfig and threaded through to both the team template (VPLTeamMembersItem.vue, read via useData()) and the blog byline pipeline (augment-authors.js, passed in from the transformPageData callback). The avatar tooltip is gated the same way: when mailto fallback is off, an unresolved contributor's tooltip shows just their name + commit count, never their email address.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Per-page subprocess spawn for repo coordinate detection
- Cached repo coordinate and GitHub username mappings once during config initialization to eliminate redundant per-page subprocess spawns and synchronous file I/O operations.
Or push these changes by commenting:
@cursor push 1f470766e3
Preview (1f470766e3)
diff --git a/config.js b/config.js
--- a/config.js
+++ b/config.js
@@ -1,6 +1,6 @@
// mods
-import {existsSync} from 'node:fs';
-import {dirname, resolve} from 'node:path';
+import {existsSync, readFileSync} from 'node:fs';
+import {dirname, isAbsolute, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
import isEmpty from 'lodash-es/isEmpty.js';
@@ -15,6 +15,7 @@
import {default as getContributors} from './utils/get-contributors.js';
import {default as getGaHeaders} from './utils/get-ga-headers.js';
import {default as getHubspotHeaders} from './utils/get-hubspot-headers.js';
+import {default as getRepoCoordinate} from './utils/get-repo-coordinate.js';
import {default as getTags} from './utils/get-tags.js';
import {default as normalizeMVB} from './utils/normalize-mvb.js';
import {default as parseLayouts} from './utils/parse-layouts.js';
@@ -195,6 +196,38 @@
debug('added hubspot tracking with %o', hubspot);
}
+ // compute repo coordinate and read GitHub username cache once for the entire
+ // build to avoid repeated subprocess spawns and synchronous file I/O in
+ // per-page getContributors calls
+ if (contributors !== false && typeof contributors === 'object') {
+ if (!contributors.repo || typeof contributors.repo === 'string') {
+ const repoCoord = getRepoCoordinate(config.gitRoot, {
+ override: typeof contributors.repo === 'string' ? contributors.repo : undefined,
+ debug: debug.extend('repo-coord'),
+ });
+ if (repoCoord) {
+ contributors.repo = repoCoord;
+ debug('cached repo coordinate %o/%o for per-page contributor resolution', repoCoord.owner, repoCoord.name);
+ }
+ }
+
+ // read the GitHub username mappings cache once if configured
+ if (contributors.cachePath && !contributors.cachedMappings) {
+ const resolvedCachePath = isAbsolute(contributors.cachePath)
+ ? contributors.cachePath
+ : resolve(config.gitRoot, contributors.cachePath);
+ if (existsSync(resolvedCachePath)) {
+ try {
+ contributors.cachedMappings = JSON.parse(readFileSync(resolvedCachePath, 'utf8'));
+ debug('cached %o email->login mappings from %o for per-page contributor resolution',
+ Object.keys(contributors.cachedMappings).length, resolvedCachePath);
+ } catch (error) {
+ debug('failed to read cache file %o: %o', resolvedCachePath, error.message);
+ }
+ }
+ }
+ }
+
// get full team info
const copts = {debug: debug.extend('get-contribs'), paths: []};
const team = contributors !== false ? await getContributors(config.gitRoot, contributors, copts) : [];
diff --git a/node/resolve-github-usernames.js b/node/resolve-github-usernames.js
--- a/node/resolve-github-usernames.js
+++ b/node/resolve-github-usernames.js
@@ -90,6 +90,7 @@
repo,
token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN,
cachePath,
+ cachedMappings,
// hard ceiling on commit-history pages we'll fetch in a single run.
// 100 pages = 10000 commits, which covers all but the largest repos.
// bumping this is cheap rate-limit wise (1 graphql point per page) but
@@ -107,7 +108,7 @@
// start with whatever's in the cache (a prior run's results). cache values
// can be a string (resolved login) or null (we tried and couldn't resolve;
// skip on subsequent runs to avoid burning api calls every build)
- const cache = readCache(cachePath, debug);
+ const cache = cachedMappings || readCache(cachePath, debug);
const result = new Map(Object.entries(cache));
// figure out which emails still need resolving — anything not in the
diff --git a/utils/get-contributors.js b/utils/get-contributors.js
--- a/utils/get-contributors.js
+++ b/utils/get-contributors.js
@@ -69,6 +69,7 @@
exclude = [],
resolveGitHub = 'auto',
cachePath,
+ cachedMappings,
repo,
token,
maxPages,
@@ -211,6 +212,7 @@
repo: repoCoord,
token,
cachePath: resolvedCachePath,
+ cachedMappings,
maxPages,
maxStalePages,
debug: debug.extend('resolve-github'),You can send follow-ups to the cloud agent here.
Previously, getContributors ran the GitHub-resolution flow inside every transformPageData call: each page spawned 'git remote get-url origin' to detect the repo coordinate and re-read the on-disk team-github.json cache. On a docs site like lando/core (~hundreds of pages and ~235 contributors) that meant hundreds of subprocess spawns and synchronous file reads per build, all returning identical results. Thread a shared 'ctx' object through the contributor pipeline: - utils/get-contributors.js: accept 'ctx' in opts. First call populates ctx.repoCoord and ctx.mappings; subsequent calls reuse them and skip the resolution work entirely. Detect token presence here so the new 'true' vs 'auto' distinction can warn loudly when the user explicitly set resolveGitHub: true but no GITHUB_TOKEN/GH_TOKEN is available. - node/add-contributors.js: forward 'ctx' to getContributors. - config.js: own a single contributorCtx that's used by the build-time team lookup AND every per-page transformPageData call. - utils/create-content-loader.js: use a content-loader-scoped ctx so the RSS feed path (createContentLoader → addContributors per blog post) also resolves once instead of once per post. After this change on this repo's own docs (44 pages, 2 blog posts): - 'parsed repo coordinate' calls: 35 → 2 - 'loaded N cached email->login mappings' (disk reads): 44 → 2 - 'reusing pre-resolved GitHub mappings' (ctx hits): 0 → 77 The two remaining resolutions are one per resolution context (page render + RSS feed gen), which is the natural floor. Also fixes the documented but previously-identical 'auto' and true modes: when resolveGitHub: true and no token is present, we now warn that resolution will fall back to cached results only — surfacing misconfiguration instead of silently degrading. Addresses Cursor Bugbot reviews on PR #101.
d49e215 to
9899fba
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Content loader omits
mailtoFallbackfor blog authors- Added mailtoFallback parameter to augmentAuthors call in create-content-loader.js matching the pattern used in config.js.
Or push these changes by commenting:
@cursor push c49c1d0f0a
Preview (c49c1d0f0a)
diff --git a/utils/create-content-loader.js b/utils/create-content-loader.js
--- a/utils/create-content-loader.js
+++ b/utils/create-content-loader.js
@@ -65,7 +65,7 @@
// parse collections
await parseCollections(data, {siteConfig, debug});
// normalize authors
- await augmentAuthors(data, {team, debug});
+ await augmentAuthors(data, {team, mailtoFallback: contributors?.mailtoFallback === true, debug});
// get stuff
const {frontmatter, html, url} = data;You can send follow-ups to the cloud agent here.
createContentLoader.transform was calling augmentAuthors() without passing mailtoFallback, so it silently defaulted to false. The parallel call from config.js's transformPageData passes 'mailtoFallback: contributors?.mailtoFallback === true', so team-page bylines and blog-post bylines diverged: a user with mailtoFallback: true (or resolveGitHub: false, which auto-resolves mailtoFallback to true) got mailto: links on team pages but not on blog posts. Mirror the same one-liner from config.js so the two paths agree. Spotted by Cursor Bugbot on PR #101.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Duplicated link resolution logic risks future divergence
- Extracted duplicated link resolution logic into a shared utility function (utils/get-contributor-link.js) that both augment-authors.js and VPLTeamMembersItem.vue now use, ensuring consistent link resolution across blog bylines and team cards.
Or push these changes by commenting:
@cursor push 310da5b012
Preview (310da5b012)
diff --git a/components/VPLTeamMembersItem.vue b/components/VPLTeamMembersItem.vue
--- a/components/VPLTeamMembersItem.vue
+++ b/components/VPLTeamMembersItem.vue
@@ -96,6 +96,7 @@
import VPIconHeart from 'vitepress/dist/client/theme-default/components/icons/VPIconHeart.vue';
import VPSocialLinks from 'vitepress/dist/client/theme-default/components/VPSocialLinks.vue';
import Link from './VPLLink.vue';
+import getContributorLink from '../utils/get-contributor-link.js';
const {member, size} = defineProps({
size: {
@@ -133,13 +134,7 @@
const maintainerClass = computed(() => member.maintainer ? 'maintainer' : '');
-const getLink = member => {
- if (member.link) return member.link;
- else if (Array.isArray(member?.links) && member.links[0]) return member.links[0].link;
- else if (member.github) return `https://github.com/${member.github}`;
- else if (member.email && mailtoFallback.value) return `mailto:${member.email}`;
- return undefined;
-};
+const getLink = member => getContributorLink(member, mailtoFallback.value);
const getAvatarTitle = member => {
let avatarTitle = `${member.name}`;
diff --git a/node/augment-authors.js b/node/augment-authors.js
--- a/node/augment-authors.js
+++ b/node/augment-authors.js
@@ -1,16 +1,9 @@
import Debug from 'debug';
+import getContributorLink from '../utils/get-contributor-link.js';
const getContributor = (id, contributors = []) => contributors.find(contributor => contributor.email === id)
?? contributors.find(contributor => contributor.name === id);
-const getLink = (author, mailtoFallback) => {
- if (author.link) return author.link;
- else if (Array.isArray(author?.links) && author.links[0]) return author.links[0].link;
- else if (author.github) return `https://github.com/${author.github}`;
- else if (author.email && mailtoFallback) return `mailto:${author.email}`;
- return undefined;
-};
-
export default async function(pageData, {
team,
// when false (the default with github resolution enabled), unresolved
@@ -28,7 +21,7 @@
frontmatter.authors = frontmatter.authors
.map(author => typeof author === 'string' ? getContributor(author, team) : author)
.filter(author => author && author !== false && author !== null)
- .map(author => ({...author, link: getLink(author, mailtoFallback)}));
+ .map(author => ({...author, link: getContributorLink(author, mailtoFallback)}));
}
// log
diff --git a/utils/get-contributor-link.js b/utils/get-contributor-link.js
new file mode 100644
--- /dev/null
+++ b/utils/get-contributor-link.js
@@ -1,0 +1,20 @@
+/**
+ * Resolves the primary link URL for a contributor/team member.
+ * Priority order:
+ * 1. Explicit `link` property
+ * 2. First entry in `links` array
+ * 3. GitHub profile URL from `github` field
+ * 4. mailto: fallback if enabled
+ * 5. undefined
+ *
+ * @param {Object} contributor - The contributor/member object
+ * @param {boolean} mailtoFallback - Whether to fallback to mailto: links
+ * @return {string|undefined} The resolved link URL
+ */
+export default function getContributorLink(contributor, mailtoFallback = false) {
+ if (contributor.link) return contributor.link;
+ else if (Array.isArray(contributor?.links) && contributor.links[0]) return contributor.links[0].link;
+ else if (contributor.github) return `https://github.com/${contributor.github}`;
+ else if (contributor.email && mailtoFallback) return `mailto:${contributor.email}`;
+ return undefined;
+}You can send follow-ups to the cloud agent here.
The link-resolution priority for contributor entries (link → links[0] →
github → mailto → undefined) was duplicated in two places:
- node/augment-authors.js (server-side, blog post bylines)
- components/VPLTeamMembersItem.vue (client-side, team cards)
The two implementations were verbatim identical, modulo parameter name
('author' vs 'member') and how mailtoFallback was unwrapped (boolean vs
Vue ref .value). We just hit a related drift bug in 9759de8 where the
two render paths diverged on a parallel concern (mailtoFallback was
threaded through one but not the other), so the maintenance risk here
is concrete, not theoretical.
Hoist the rules into utils/get-author-link.js as a pure function and
have both files import it. Pure JS works in both Node and browser
contexts; no behavior change.
Verified: built dist/team.html has 4 distinct github.com/<user> links
and 0 mailto: links, identical to before. Built blog byline still
resolves to https://github.com/pirog.
Spotted by Cursor Bugbot on PR #101.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unvalidated cache parse can crash build on corruption
- Added validation after JSON.parse to ensure the result is a plain object before returning it, preventing TypeError when Object.entries is called on the cache.
Or push these changes by commenting:
@cursor push 2f9848e5ab
Preview (2f9848e5ab)
diff --git a/node/resolve-github-usernames.js b/node/resolve-github-usernames.js
--- a/node/resolve-github-usernames.js
+++ b/node/resolve-github-usernames.js
@@ -29,6 +29,7 @@
if (!cachePath || !existsSync(cachePath)) return {};
try {
const data = JSON.parse(readFileSync(cachePath, 'utf8'));
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return {};
debug('loaded %o cached email->login mappings from %o', Object.keys(data).length, cachePath);
return data;
} catch (error) {You can send follow-ups to the cloud agent here.
A cache file containing valid JSON that isn't a plain object (null, array, primitive) would silently produce a Map keyed by array indices or characters, defeating the cache and re-hitting the GitHub API on every build. Validate the parsed value before trusting it.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Stale GitHub mappings when per-page contributors differ from global
- Added logic to resolve and merge new emails not yet in ctx.mappings on subsequent per-page calls.
- ✅ Fixed: Avatar URL format may not resolve correctly
- Changed avatar URL from avatars.githubusercontent.com/{login} to the documented github.com/{login}.png format.
Or push these changes by commenting:
@cursor push bf6dd58d88
Preview (bf6dd58d88)
diff --git a/utils/get-contributors.js b/utils/get-contributors.js
--- a/utils/get-contributors.js
+++ b/utils/get-contributors.js
@@ -39,7 +39,7 @@
if (!login) continue;
contributor.github = login;
if (isGravatarAvatar(contributor.avatar)) {
- contributor.avatar = `https://avatars.githubusercontent.com/${login}`;
+ contributor.avatar = `https://github.com/${login}.png`;
}
contributor.links = Array.isArray(contributor.links) ? contributor.links : [];
if (!contributor.links.some(link => link?.icon === 'github')) {
@@ -222,6 +222,35 @@
}
} else {
debug('reusing pre-resolved GitHub mappings (%o entries)', ctx.mappings?.size ?? 0);
+ // resolve any new emails not yet in mappings (e.g., from per-page includes)
+ if (ctx.repoCoord && ctx.mappings !== null) {
+ const newEmailsToResolve = data
+ .filter(c => !c.links?.some(link => link?.icon === 'github'))
+ .map(c => c.email)
+ .filter(Boolean)
+ .filter(email => !ctx.mappings.has(email));
+
+ if (newEmailsToResolve.length > 0) {
+ debug('found %o new emails to resolve', newEmailsToResolve.length);
+ const apiToken = token ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
+ const resolvedCachePath = cachePath
+ ? (isAbsolute(cachePath) ? cachePath : resolve(cwd, cachePath))
+ : undefined;
+ const newMappings = await resolveGitHubUsernames(newEmailsToResolve, {
+ repo: ctx.repoCoord,
+ token: apiToken,
+ cachePath: resolvedCachePath,
+ maxPages,
+ maxStalePages,
+ debug: debug.extend('resolve-github'),
+ });
+ if (newMappings) {
+ for (const [email, login] of newMappings.entries()) {
+ ctx.mappings.set(email, login);
+ }
+ }
+ }
+ }
}
// applies mappings AND scrapes hand-configured github linksYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 5b6a0ed. Configure here.
|
|
||
| // applies mappings AND scrapes hand-configured github links | ||
| applyGitHubLogins(data, ctx.mappings); | ||
| } |
There was a problem hiding this comment.
Stale GitHub mappings when per-page contributors differ from global
Low Severity
The emailsToResolve list is built only from contributors present in the first getContributors call (global team, paths: []). Once ctx.mappings is set, subsequent per-page calls skip resolution entirely. If a per-page call discovers a contributor (via page-specific frontmatter.contributors.include) whose email was never in the global data set, that email will never appear in ctx.mappings and will silently miss GitHub resolution without any retry mechanism across calls within the same build.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5b6a0ed. Configure here.
| if (!login) continue; | ||
| contributor.github = login; | ||
| if (isGravatarAvatar(contributor.avatar)) { | ||
| contributor.avatar = `https://avatars.githubusercontent.com/${login}`; |
There was a problem hiding this comment.
Avatar URL format may not resolve correctly
Medium Severity
The avatar URL https://avatars.githubusercontent.com/${login} uses the GitHub login directly in the path. GitHub's documented avatar URL format is https://github.com/${login}.png (by username) or https://avatars.githubusercontent.com/u/${numeric_id} (by user ID). The format used here may not consistently resolve to a valid image, potentially breaking avatar display for all GitHub-resolved contributors.
Reviewed by Cursor Bugbot for commit 5b6a0ed. Configure here.




Closes #78. Team-card avatars (and blog post bylines) now link to GitHub profiles and use GitHub profile pictures when a contributor's commit email matches a GitHub user.
Resolution uses cached GraphQL calls to GitHub's
repository.history. First build walks history once and writesdocs/.vitepress/cache/team-github.json; subsequent builds make zero API calls.When GitHub resolution is on (default), unresolved contributors get no avatar link rather than a
mailto:— opting into GitHub avatars opts you out of email exposure on public pages. Configurable.New
themeConfig.contributorsoptionsresolveGitHub'auto''auto'tries whenGITHUB_TOKENis set, else skipscachePath'docs/.vitepress/cache/team-github.json'repogit remote'owner/name'overridemailtoFallback'auto''auto'= off whenresolveGitHubis onmaxPages100maxStalePages10Verified
mailto:onteam.htmlBackward-compatible: set
resolveGitHub: false(ormailtoFallback: true) for legacy behavior.Note
Medium Risk
Adds build-time GitHub GraphQL lookups and on-disk caching to enrich contributor identity, which can affect builds and external API usage if misconfigured (token/repo/caching). UI link behavior also changes from
mailto:to GitHub/none depending on config.Overview
Upgrades contributor identity to GitHub-backed profiles. Team member avatars and blog bylines now prefer linking to GitHub profiles (and show
@usernamein tooltips) via a sharedgetAuthorLinkhelper, withmailto:only used whenthemeConfig.contributors.mailtoFallbackis enabled.Adds optional GitHub username resolution with caching and better performance.
getContributorscan now resolve commit emails to GitHub logins via a paginated GraphQL history walk, persist results tothemeConfig.contributors.cachePath, and reuse a build-widectxso resolution (andgit remoterepo sniffing) runs once per build instead of per page/content-loader item; explicitresolveGitHub: truenow warns when noGITHUB_TOKEN/GH_TOKENis set.Config/docs updates. Default/preset contributor configs gain
resolveGitHub,cachePath, andmailtoFallback(with'auto'behavior), and documentation/changelog are updated to describe the new options and behavior.Reviewed by Cursor Bugbot for commit 5b6a0ed. Bugbot is set up for automated code reviews on this repo. Configure here.