Skip to content
Docs Portfolio

Building a GitHub Contribution Graph with Next.js Server Components

When building a portfolio, showing your GitHub activity tells a story about your consistency, the technologies you work with, and how you balance personal and professional projects. In this guide, I walk you through building a production-ready GitHub contribution graph using Next.js 14 Server Components and TypeScript.

Unlike client-side rendering, Server Components allow us to fetch data directly on the server, keeping our GitHub API token secure and improving initial page load performance. This is especially important for portfolio sites where first impressions matter.

GitHub’s GraphQL API is more efficient than the REST API for fetching contribution data. Here’s why:

  • Single Request: Get all the data you need in one query
  • Precise Fields: Request only what you need, reducing payload size
  • Strongly Typed: GraphQL schemas provide built-in documentation

First, create a Personal Access Token with read:user scope:

Terminal window
# Visit https://github.com/settings/tokens
# Generate a new token with read:user scope
# Store it in .env.local
GITHUB_TOKEN=ghp_your_token_here

One of the keys to maintainable code is proper typing. Here’s our contribution data structure:

interface ContributionDay {
date: string;
contributionCount: number;
contributionLevel: 'NONE' | 'FIRST_QUARTILE' |
'SECOND_QUARTILE' | 'THIRD_QUARTILE' | 'FOURTH_QUARTILE';
}

GitHub provides contribution levels in quartiles, which we’ll map to color intensities in our visualization.

Here’s the core function that talks to GitHub’s API:

export async function fetchGitHubContributions(
username: string
): Promise<ContributionCalendar | null> {
const query = `
query($username: String!) {
user(login: $username) {
contributionsCollection {
contributionCalendar {
totalContributions
weeks {
contributionDays {
date
contributionCount
contributionLevel
}
}
}
}
}
}
`;
const response = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Authorization': `Bearer \${process.env.GITHUB_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables: { username } }),
// ISR: Cache for 1 hour
next: { revalidate: 3600 },
});
// Error handling omitted for brevity
const result = await response.json();
return result.data.user.contributionsCollection.contributionCalendar;
}

The next: { revalidate: 3600 } option enables Incremental Static Regeneration (ISR), caching the data for one hour. This reduces API calls and improves performance.

Many developers have separate personal and work GitHub accounts. Let’s aggregate them:

export async function aggregateContributions(
usernames: string[]
): Promise<{ totalContributions: number; byAccount: Record<string, number> }> {
const results = await Promise.allSettled(
usernames.map(username => fetchGitHubContributions(username))
);
let totalContributions = 0;
const byAccount: Record<string, number> = {};
results.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value) {
const count = result.value.totalContributions;
byAccount[usernames[index]] = count;
totalContributions += count;
}
});
return { totalContributions, byAccount };
}

Using Promise.allSettled ensures that if one account fails, we still get data from the others.

Now for the fun part—rendering the contribution calendar:

export default async function GitHubContributions({
username,
days = 90
}: GitHubContributionsProps) {
const usernames = Array.isArray(username) ? username : [username];
const aggregated = await aggregateContributions(usernames);
const primaryCalendar = await fetchGitHubContributions(usernames[0]);
const recentDays = getRecentContributions(primaryCalendar, days);
const weeks = Math.ceil(recentDays.length / 7);
return (
<div className="grid gap-1" style={{
gridTemplateColumns: `repeat(\${weeks}, minmax(10px, 1fr))`,
gridTemplateRows: 'repeat(7, minmax(10px, 1fr))',
gridAutoFlow: 'column',
}}>
{recentDays.map((day) => (
<div
key={day.date}
className={`w-3 h-3 rounded-sm \${levelColors[day.contributionLevel]}`}
title={`\${day.contributionCount} contributions`}
/>
))}
</div>
);
}

The CSS Grid layout automatically flows contributions from top to bottom, left to right, just like GitHub’s native visualization.

  1. Server-Side Rendering - Data fetched before the page reaches the client
  2. ISR Caching - One-hour cache reduces API calls by 99%
  3. Minimal JavaScript - Server Component means zero client-side JS for this feature
  4. Optimized Query - GraphQL fetches only required fields

Using Tailwind’s dark mode variants:

const levelColors = {
NONE: 'bg-neutral-100 dark:bg-neutral-800',
FIRST_QUARTILE: 'bg-green-200 dark:bg-green-900',
// ... more levels
};

Always handle API failures gracefully:

if (!primaryCalendar) {
return (
<div className="p-4 border rounded-lg">
<p className="text-sm text-neutral-600">
Unable to load contribution data.
Please check your GitHub token configuration.
</p>
</div>
);
}
  1. GraphQL > REST for Nested Data - One query beats multiple REST calls
  2. Server Components Are Powerful - Keep secrets secure, improve performance
  3. Type Safety Matters - TypeScript caught several bugs during development
  4. Cache Strategically - ISR balances freshness with performance
  5. Fail Gracefully - Always provide fallback UI for API errors

As a Documentation Engineer, building this feature taught me how developers actually use our API docs. The disconnect between reference documentation and real-world implementation patterns became clear, a lesson I’ll carry into future docs work. When you write the integration guide yourself, you discover which concepts need more explanation, which error messages are confusing, and where examples would save hours of debugging.

Some ideas for taking this further:

  • Interactive Tooltips - Show detailed contribution breakdown on hover
  • Commit Message Analysis - Display most-used technologies/keywords
  • Contribution Streaks - Highlight longest streak of consecutive days
  • Organization Contributions - Include work from private repos (with proper scoping)

This implementation demonstrates the power of Next.js Server Components for building interactive, data-driven portfolio features. By combining GitHub’s GraphQL API with TypeScript and modern React patterns, we’ve created a performant, maintainable solution that showcases both engineering and content skills.


Tech Stack: Next.js 14, TypeScript, Tailwind CSS, GitHub GraphQL API

Live Demo: View on my portfolio

Source Code: github.ts on GitHub