GitHub data appears in two places on this portfolio. The projects page shows a profile summary and repository-level details when they match a project. The dashboard includes public repository and star totals among its wider activity metrics.

The current path is intentionally small: lib/github.js talks to GitHub, Pages Router API routes shape and cache the results, and SWR loads those routes in the browser. There is no /api/github-profile endpoint, and the client never calls GitHub directly.

One server-side GitHub helper

lib/github.js uses the platform fetch implementation rather than a GitHub SDK. Every request includes GitHub’s JSON accept header and a user-agent. If GITHUB_TOKEN exists, the helper adds it as a bearer token:

function headers() {
  const h = {
    Accept: 'application/vnd.github+json',
    'User-Agent': 'my-site-dashboard',
  }
  if (process.env.GITHUB_TOKEN) {
    h.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`
  }
  return h
}

Keeping this code on the server has two benefits visible in the implementation: the optional token is not exposed to browser JavaScript, and components receive site-specific objects instead of raw GitHub responses.

The helper can fetch a user, public events, all owned repositories, or a requested set of repository slugs. fetchAllRepos requests up to 100 repositories per page and stops after three pages for a safety cap of 300. aggregateUserStats requests the user and repositories concurrently, then reduces the repository list into star, fork, size, and language totals.

The returned profile shape includes identity fields, account creation time, public repository and follower counts, total stars and forks, the top five languages by repository count, a five-repository sample, and fetchedAt. That normalization is why the UI can use names such as avatarUrl rather than GitHub’s raw avatar_url.

The projects route returns both views

pages/api/project-repos.js has a curated repoMap from project titles to GitHub owner/name slugs. It deduplicates those slugs and requests repository details at the same time as the profile aggregate:

const [data, profile] = await Promise.all([
  fetchReposBySlugs(slugs),
  aggregateUserStats('mofdabo'),
])

res.status(200).json({
  repos: data,
  repoMap,
  profile: profile.error ? null : profile,
})

This is the actual replacement for the stale idea of a separate profile endpoint. One /api/project-repos response gives the projects page the repository array and the profile summary it needs.

fetchReposBySlugs handles each slug independently. A failed lookup becomes { slug, error: true }, allowing other repositories to remain usable. A profile aggregation failure becomes null at the route boundary. If the route itself throws, it returns a 500 response with a stable error message.

The projects component uses:

useSWR('/api/project-repos', fetcher, {
  dedupingInterval: 300000,
})

There is no refreshInterval there. During a mounted client session, equivalent requests are deduplicated for five minutes. The server response also sets s-maxage=900 and stale-while-revalidate=3600, allowing a shared cache to serve the response for 15 minutes and reuse stale data while revalidating for up to an hour.

When rendering a project card, the page extracts a GitHub slug from the project URL, finds the corresponding normalized repository object, and displays stars and forks only if that object exists without an error. The profile card similarly renders only when data.profile is present.

GitHub inside aggregate metrics

The same helper supports /api/metrics and /api/dashboard. /api/metrics combines Markdown post totals, the local project count, calculated years of experience, and GitHub repository and star totals. It uses a 600-second shared cache with a 300-second stale window.

/api/dashboard combines GitHub with Spotify and Steam. If aggregateUserStats returns an error, that handler substitutes zero values for publicRepos and stars and marks the GitHub source as unavailable. The rest of the dashboard can still load. The response is cached for 60 seconds with 30 seconds of stale-while-revalidate, matching the page’s more active role.

The dashboard’s SWR call refreshes /api/dashboard every 60 seconds and deduplicates for 30 seconds. That does not mean every browser refresh must reach GitHub: SWR governs client requests, while API cache headers govern reuse of the server response.

What this integration deliberately does not claim

The repository does not include a client-side GitHub SDK, a database of historical star counts, or a custom in-memory slug cache. It shows a current aggregate and selected repository metadata, with graceful degradation when GitHub is unavailable. It also does not promise that every displayed value was fetched at the exact moment it was rendered; cache windows and fetchedAt are part of the design.

That scope fits the rest of the site. The architecture overview explains the static-core/dynamic-edge split, while Static Generation and Dynamic Data in This Portfolio covers how SWR and API caching differ from pre-rendering the blog.