Welcome! Type "help" for available commands.
$
Loading terminal interface...
Back to Blog

Adding a GitHub Contribution Graph to Next.js

April 2, 2025
William Callahan

Software engineer, founder, and leadership background in finance/tech. Based in San Francisco.

nextjsgithubgraphqlapireacttypescriptcaching
Adding a GitHub Contribution Graph to Next.js

Adding your GitHub contribution graph to a personal site is a nice touch. Since GitHub doesn't offer a direct embed, you'll need a custom solution. This guide shows how to fetch contribution data using GitHub's GraphQL API and display it in your Next.js app.

1: Fetching Contribution Data with GraphQL

The best way to get contribution data is via GitHub's GraphQL API. You'll need a personal access token with permission to read user contribution data. Store this securely, for example, as an environment variable (GITHUB_ACCESS_TOKEN_COMMIT_GRAPH).

Here's the query to get the contribution calendar for a specific user and date range:

// lib/github.ts (example location)
import { graphql } from "@octokit/graphql"; // Assuming you use @octokit/graphql

interface GitHubGraphQLContributionResponse {
  user: {
    contributionsCollection: {
      contributionCalendar: {
        weeks: Array<{
          contributionDays: Array<{
            contributionCount: number;
            contributionLevel: string; // e.g., NONE, FIRST_QUARTILE
            date: string; // YYYY-MM-DD
          }>;
        }>;
        totalContributions: number;
      };
    };
  };
}

interface ContributionDay {
  date: string;
  count: number;
  level: number; // 0-4
}

const GITHUB_USERNAME = 'your-github-username'; // Replace with your username
const GITHUB_API_TOKEN = process.env.GITHUB_ACCESS_TOKEN_COMMIT_GRAPH;

export async function fetchGitHubContributions(fromDate: string, toDate: string): Promise<{ contributions: ContributionDay[], totalContributions: number }> {
  if (!GITHUB_API_TOKEN) {
    throw new Error("Missing GITHUB_ACCESS_TOKEN_COMMIT_GRAPH environment variable.");
  }

  const { user } = await graphql<GitHubGraphQLContributionResponse>(
    `
      query($username: String!, $from: DateTime!, $to: DateTime!) {
        user(login: $username) {
          contributionsCollection(from: $from, to: $to) {
            contributionCalendar {
              weeks {
                contributionDays {
                  contributionCount
                  contributionLevel
                  date
                }
              }
              totalContributions
            }
          }
        }
      }
    `,
    {
      username: GITHUB_USERNAME,
      from: `${fromDate}T00:00:00Z`,
      to: `${toDate}T23:59:59Z`,
      headers: {
        authorization: `bearer ${GITHUB_API_TOKEN}`,
      },
    }
  );

  if (!user?.contributionsCollection) {
    throw new Error("Incomplete data received from GitHub GraphQL.");
  }

  const contributionDays = user.contributionsCollection.contributionCalendar.weeks.flatMap(
    (week) => week.contributionDays
  );

  const mapContributionLevel = (level: string): number => {
    switch (level) {
      case 'NONE': return 0;
      case 'FIRST_QUARTILE': return 1;
      case 'SECOND_QUARTILE': return 2;
      case 'THIRD_QUARTILE': return 3;
      case 'FOURTH_QUARTILE': return 4;
      default: return 0;
    }
  };

  const contributions: ContributionDay[] = contributionDays.map((day) => ({
    date: day.date,
    count: day.contributionCount,
    level: mapContributionLevel(day.contributionLevel),
  }));

  return {
    contributions,
    totalContributions: user.contributionsCollection.contributionCalendar.totalContributions
  };
}

This function fetches the data and transforms the contributionLevel strings (like FIRST_QUARTILE) into simple numbers (0-4) for easier rendering.

2: Caching the Data

To avoid hitting the GitHub API on every request, cache the results server-side. An in-memory cache like node-cache works well for this. Set a reasonable Time-To-Live (TTL), like 24 hours. You can also add a way to manually bust the cache if needed.

Here's an example API route in Next.js:

// app/api/github-activity/route.ts (example location)
import { NextResponse } from 'next/server';
import cache from '@/lib/cache'; // Your cache implementation
import { fetchGitHubContributions } from '@/lib/github'; // The function from Step 1

interface GitHubActivityApiResponse {
  source: 'graphql' | 'cache';
  data: ContributionDay[];
  totalContributions: number;
}

const GITHUB_ACTIVITY_CACHE_KEY = 'github_activity_data';
const CACHE_TTL_DAILY = 24 * 60 * 60; // 24 hours in seconds

export async function GET(request: Request) {
  const url = new URL(request.url);
  const refreshCache = url.searchParams.get('refresh') === 'true';

  if (refreshCache) {
    cache.del(GITHUB_ACTIVITY_CACHE_KEY);
  }

  const cachedData = cache.get<GitHubActivityApiResponse>(GITHUB_ACTIVITY_CACHE_KEY);
  if (cachedData && !refreshCache) {
    // Add source indication for debugging/transparency
    return NextResponse.json({ ...cachedData, source: 'cache' });
  }

  try {
    const today = new Date();
    const oneYearAgo = new Date(today);
    oneYearAgo.setFullYear(today.getFullYear() - 1);

    const fromDate = oneYearAgo.toISOString().split('T')[0]; // YYYY-MM-DD
    const toDate = today.toISOString().split('T')[0];       // YYYY-MM-DD

    const { contributions, totalContributions } = await fetchGitHubContributions(fromDate, toDate);

    const activityData: GitHubActivityApiResponse = {
      source: 'graphql',
      data: contributions,
      totalContributions: totalContributions
    };

    cache.set(GITHUB_ACTIVITY_CACHE_KEY, activityData, CACHE_TTL_DAILY);
    return NextResponse.json(activityData);

  } catch (error) {
    console.error('Error fetching GitHub contributions:', error);
    // Return an error response or potentially stale cache data if available
    return NextResponse.json(
      { error: 'Failed to fetch GitHub activity', details: (error as Error).message },
      { status: 500 }
    );
  }
}

This API route first checks the cache. If data is missing or a refresh is requested, it fetches fresh data from GitHub using the function created in Step 1, caches it, and then returns it.

3: Rendering the Graph

With the data fetched (likely via a client-side fetch to your API route or passed as props from a server component), you can render the graph. A simple grid using CSS Grid and Tailwind CSS works well.

// components/ContributionGraph.tsx (example component)
import React from 'react';

interface ContributionDay {
  date: string;
  count: number;
  level: number; // 0-4
}

const getLevelColor = (level: number): string => {
  // Tailwind classes for different contribution levels
  // Adjust colors to match your site's theme
  switch (level) {
    case 0: return 'bg-gray-100 dark:bg-gray-800'; // No contributions
    case 1: return 'bg-green-200 dark:bg-green-900'; // Low
    case 2: return 'bg-green-400 dark:bg-green-700'; // Medium
    case 3: return 'bg-green-600 dark:bg-green-500'; // High
    case 4: return 'bg-green-800 dark:bg-green-300'; // Very high
    default: return 'bg-gray-100 dark:bg-gray-800';
  }
};

interface ContributionGraphProps {
  data: ContributionDay[];
  totalContributions: number;
  isLoading?: boolean;
}

export function ContributionGraph({ data, totalContributions, isLoading }: ContributionGraphProps) {
  if (isLoading) {
    // Optional: Add a loading skeleton state
    return <div className="p-2">Loading contributions...</div>;
  }

  if (!data || data.length === 0) {
    return <div className="p-2">Could not load contribution data.</div>;
  }

  return (
    <div>
      <p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
        {totalContributions.toLocaleString()} contributions in the last year
      </p>
      <div className="grid grid-flow-col grid-rows-7 gap-1 p-2 border rounded-md dark:border-gray-700 overflow-x-auto bg-white dark:bg-gray-900/50">
        {data.map((day) => (
          <div
            key={day.date}
            className={`w-3 h-3 rounded-sm ${getLevelColor(day.level)}`}
            title={`${day.count} contribution${day.count !== 1 ? 's' : ''} on ${day.date}`} // Tooltip
          />
        ))}
      </div>
    </div>
  );
}

This component maps each ContributionDay to a colored square, using the level (0-4) to determine the background color via the getLevelColor helper.

Bonus: Fixing Hydration Errors

Sometimes when making components interactive, like wrapping them in links, you might encounter React hydration errors due to invalid HTML nesting (e.g., an <a> inside another <a>).

Problematic (can cause errors):

<a href="URL">
  <div> {isLoading ? <a href="URL">Loading...</a> : content } </div>
</a>

Solution (use a clickable div):

const navigateToUrl = (url: string) => {
  window.open(url, '_blank', 'noopener,noreferrer');
};

<div
  onClick={() => navigateToUrl(URL)}
  className="cursor-pointer" // Make it look clickable
  role="button"
  tabIndex={0} // Make it focusable
  onKeyDown={(e) => e.key === 'Enter' && navigateToUrl(URL)} // Keyboard accessible
>
  {isLoading ? <span>Loading...</span> : content}
</div>

Using a div with an onClick handler and proper accessibility attributes (role, tabIndex, onKeyDown) achieves the same result without the nesting issues.

Similar Content

HomeExperienceEducationCVProjectsBookmarksInvestmentsContactBlog
Welcome! Type "help" for available commands.
$
Loading terminal interface...

Similar Content

Related Investments

INV
December 31, 2021
Canua

Canua

Financial data analytics platform for consumers and investment professionals.

financepre-seedrealized+8 more
INV
December 31, 2022
AngelList

AngelList

Platform connecting startups with investors, talent, and resources for fundraising and growth.

investment platformsotheractive+8 more
aVenture
INV
December 31, 2021
Sudrania

Sudrania

Fund administration and accounting platform for investment managers.

financeseries aactive+7 more

Related Bookmarks

LINK
July 28, 2025
Streaming Custom Data

Streaming Custom Data

Learn how to stream custom data from the server to the client.

real-time dataai sdksstreaming apis+7 more
v5.ai-sdk.dev
LINK
July 22, 2025
GitHub - mcp-use/mcp-use: mcp-use is the easiest way to interact with mcp servers with custom agents

GitHub - mcp-use/mcp-use: mcp-use is the easiest way to interact with mcp servers with custom agents

mcp-use is the easiest way to interact with mcp servers with custom agents - mcp-use/mcp-use

githubopen source projectsai agents+7 more
github.com
LINK
June 20, 2025
GitHub - JudiniLabs/mcp-code-graph: MCP Server for code graph analysis and visualization by CodeGPT

GitHub - JudiniLabs/mcp-code-graph: MCP Server for code graph analysis and visualization by CodeGPT

MCP Server for code graph analysis and visualization by CodeGPT - JudiniLabs/mcp-code-graph

developer toolscode graph analysisknowledge graphs+7 more
github.com

Related Projects

PRJ
repo-tokens-calculator

repo-tokens-calculator

CLI token counter (Python + tiktoken + uv) with pretty summary

clipythontiktoken+11 more
PRJ
williamcallahan.com

williamcallahan.com

Interactive personal site with beautiful terminal/code components & other dynamic content

graph indexs3 object storageinteractive app+11 more
PRJ
SearchAI

SearchAI

AI-powered web search with a contextual chat assistant

aiweb searchchat assistant+14 more

Related Articles

BLOG
September 25, 2025
How to Secure Environment Variables for LLMs, MCPs, and AI Tools Using 1Password or Doppler

How to Secure Environment Variables for LLMs, MCPs, and AI Tools Using 1Password or Doppler

Stop hardcoding API keys in MCP configs and AI tool settings. Learn how to use 1Password CLI or Doppler to inject secrets just-in-time for Claude, Cur...

security1passworddoppler+13 more
William CallahanWilliam Callahan
BLOG
November 9, 2025
Attempting to Design a Back-end with Cleaner Architecture Rules and Boundaries

Attempting to Design a Back-end with Cleaner Architecture Rules and Boundaries

How I'm learning to build with better software architecture design principles (while 'moving fast and breaking things').

backendarchitecturespring boot+13 more
William CallahanWilliam Callahan
BLOG
April 7, 2025
Building an Interactive Terminal in Next.js

Building an Interactive Terminal in Next.js

Building a simulated terminal UI with macOS-like window controls in Next.js, focusing on global state management and hydration patterns.

nextjsreacttypescript+16 more
William CallahanWilliam Callahan