Andy Hinkle

Andy Hinkle

Laravel Developer


October 18, 2025

Display Your Public GitHub Contributions on Your Website

As developers, our GitHub contributions tell a story. They show what we're working on, what communities we're contributing to, and how active we are in open source. So why not showcase those contributions directly on your personal website?

I recently added a "Recent Contributions" section to my site, and a few folks have reached out asking how I did that. It's been a great way to highlight my recent work. Here's how you can do the same with Laravel.

GitHub Contributions

In terms of implementation, we'll be using:

  • GitHub's GraphQL API to fetch public pull requests
  • An Artisan command to fetch and cache the data
  • A Blade component to display the contributions

Why Use GitHub's GraphQL API?

You might be wondering: "Doesn't GitHub have a REST API?" They do, but here's why GraphQL is the better choice for this use case:

  1. Single Request - Get exactly the data you need in one API call instead of multiple REST endpoints
  2. Precise Data Fetching - Request only the fields you need (title, URL, merged date, etc.) without over-fetching
  3. Flexible Filtering - GitHub's search syntax works seamlessly with GraphQL queries

Plus, it's actually simpler to work with once you get the hang of it.

Step 1: Create the Artisan Command

First, let's create an Artisan command to fetch contributions from GitHub. This command will run on a schedule and cache the results so we're not hitting the API on every page load.

1php artisan make:command GitHub/FetchGitHubContributionsCommand

Here's the implementation:

1<?php
2 
3namespace App\Console\Commands\GitHub;
4 
5use Illuminate\Console\Command;
6use Illuminate\Support\Facades\Cache;
7use Illuminate\Support\Facades\Http;
8 
9class FetchGitHubContributionsCommand extends Command
10{
11 protected $signature = 'fetch:github-contributions';
12 protected $description = 'Fetch recent GitHub contributions and stores them in cache';
13 
14 public function handle(): int
15 {
16 $this->info('Fetching GitHub Contributions...');
17 
18 $data = tap($this->fetchGitHubPublicPullRequests(),
19 fn ($data) => Cache::put('github_contributions', $data, now()->addDay())
20 );
21 
22 $this->info('GitHub contributions fetched successfully.');
23 
24 return 0;
25 }
26 
27 protected function fetchGitHubPublicPullRequests(): array
28 {
29 $token = config('services.github.token');
30 
31 if (! is_string($token)) {
32 return [];
33 }
34 
35 $response = Http::withToken($token)
36 ->throw()
37 ->post('https://api.github.com/graphql', [
38 'query' => $this->graphqlPullRequestQuery(),
39 ]);
40 
41 $data = $response->json();
42 
43 if (! is_array($data)) {
44 return [];
45 }
46 
47 // Navigate the GraphQL response structure
48 $nodes = $data['data']['search']['nodes'] ?? null;
49 
50 if (! is_array($nodes)) {
51 return [];
52 }
53 
54 // Filter out null entries that sometimes appear in search results
55 return array_values(array_filter($nodes, fn ($node) => $node !== null));
56 }
57 
58 protected function graphqlPullRequestQuery(): string
59 {
60 return <<<'GRAPHQL'
61 query {
62 search(query: "is:pr is:merged author:YOUR_GITHUB_USERNAME", type: ISSUE, first: 10) {
63 nodes {
64 ... on PullRequest {
65 title
66 url
67 mergedAt
68 bodyText
69 additions
70 deletions
71 repository {
72 name
73 isPrivate
74 owner {
75 login
76 }
77 }
78 }
79 }
80 }
81 }
82 GRAPHQL;
83 }
84}

Don't forget to add your GitHub token to your .env file:

1GITHUB_TOKEN=your_github_personal_access_token

And register it in config/services.php:

1'github' => [
2 'token' => env('GITHUB_TOKEN'),
3],

Step 2: Filtering Your Contributions

GitHub's search syntax is powerful. Here are some practical examples of when you might want to filter:

1# Exclude public work repositories (common use case)
2query: "is:pr is:merged author:yourname -org:your-company"
3 
4# Only show contributions to specific organizations
5query: "is:pr is:merged author:yourname org:laravel"
6 
7# Exclude multiple repositories
8query: "is:pr is:merged author:yourname -repo:owner/private-repo -repo:owner/another-repo"
9 
10# Only merged PRs from the last 6 months
11query: "is:pr is:merged author:yourname merged:>2024-04-18"

In my case, I wanted to exclude PRs to my own personal website repository (because it's not particularly interesting to visitors), so my query looks like this:

1query: "is:pr is:merged author:ahinkle -repo:ahinkle/andyhinkle.com"

The -repo:owner/name syntax excludes that specific repository from results. Simple and effective.

Step 3: Create the Blade Component

Now let's create a component to format and display the contributions. Create a component class:

1php artisan make:component RecentGithubContributions

Here's the implementation:

1<?php
2 
3namespace App\View\Components;
4 
5use Closure;
6use Illuminate\Contracts\View\View;
7use Illuminate\Support\Carbon;
8use Illuminate\Support\Collection;
9use Illuminate\Support\Facades\Cache;
10use Illuminate\View\Component;
11 
12class RecentGithubContributions extends Component
13{
14 public function render(): View|Closure|string
15 {
16 return view('components.recent-github-contributions', [
17 'contributions' => $this->getContributions(),
18 ]);
19 }
20 
21 protected function getContributions(): Collection
22 {
23 $contributions = Cache::get('github_contributions', []);
24 
25 if (! is_array($contributions)) {
26 $contributions = [];
27 }
28 
29 return $this->formatContributions($contributions);
30 }
31 
32 protected function formatContributions(array $contributions): Collection
33 {
34 return collect($contributions)
35 ->map(fn ($contribution): array => $this->formatContribution($contribution));
36 }
37 
38 private function formatContribution(mixed $contribution): array
39 {
40 if (! is_array($contribution)) {
41 return $this->emptyContribution();
42 }
43 
44 $repository = $contribution['repository'] ?? [];
45 $owner = $repository['owner'] ?? [];
46 $ownerLogin = $owner['login'] ?? '';
47 
48 return [
49 'title' => $contribution['title'] ?? '',
50 'url' => $contribution['url'] ?? '',
51 'merged_at' => is_string($contribution['mergedAt'] ?? null)
52 ? Carbon::parse($contribution['mergedAt'])
53 : now(),
54 'body' => $contribution['bodyText'] ?? '',
55 'additions' => $contribution['additions'] ?? 0,
56 'deletions' => $contribution['deletions'] ?? 0,
57 'repository' => $repository['name'] ?? '',
58 'owner' => $ownerLogin,
59 'avatar_url' => $ownerLogin !== ''
60 ? "https://github.com/{$ownerLogin}.png"
61 : '',
62 ];
63 }
64 
65 private function emptyContribution(): array
66 {
67 return [
68 'title' => '',
69 'url' => '',
70 'merged_at' => now(),
71 'body' => '',
72 'additions' => 0,
73 'deletions' => 0,
74 'repository' => '',
75 'owner' => '',
76 'avatar_url' => '',
77 ];
78 }
79}

Step 4: Create the Blade View

Create the view file at resources/views/components/recent-github-contributions.blade.php:

1<div class="space-y-4">
2 @forelse($contributions as $contribution)
3 <a
4 href="{{ $contribution['url'] }}"
5 target="_blank"
6 class="block p-4 rounded-lg border border-gray-700 hover:border-gray-600 transition"
7 >
8 <div class="flex items-start gap-3">
9 @if($contribution['avatar_url'])
10 <img
11 src="{{ $contribution['avatar_url'] }}"
12 alt="{{ $contribution['owner'] }}"
13 class="w-10 h-10 rounded-full"
14 >
15 @endif
16 
17 <div class="flex-1">
18 <h3 class="font-medium text-gray-100">
19 {{ $contribution['title'] }}
20 </h3>
21 
22 <p class="text-sm text-gray-400 mt-1">
23 {{ $contribution['owner'] }}/{{ $contribution['repository'] }}
24 </p>
25 
26 <div class="flex items-center gap-4 mt-2 text-xs text-gray-500">
27 <span>Merged {{ $contribution['merged_at']->diffForHumans() }}</span>
28 <span class="text-green-500">+{{ $contribution['additions'] }}</span>
29 <span class="text-red-500">-{{ $contribution['deletions'] }}</span>
30 </div>
31 </div>
32 </div>
33 </a>
34 @empty
35 <p class="text-gray-400">No recent contributions found.</p>
36 @endforelse
37</div>

Step 5: Add to Your Homepage

Now you can add the component anywhere on your site. For example, on your homepage:

1<section>
2 <h2 class="text-2xl font-bold mb-4">Recent Contributions</h2>
3 
4 <x-recent-github-contributions />
5</section>

Step 6: Schedule the Command

Add the command to your scheduler in routes/console.php (or app/Console/Kernel.php if using Laravel 10 or earlier):

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('fetch:github-contributions')->daily();

For immediate testing, you can run the command manually:

1php artisan fetch:github-contributions

The Result

You now have a dynamic section on your website showcasing your latest public contributions. It updates daily, uses efficient caching, and only fetches the data you need from GitHub.

Is this the "best" way to implement this feature? Probably not. There are certainly more sophisticated approaches you could take:

  • Use a dedicated package like Spatie's GitHub package for more robust API interactions
  • Store contributions in a database table for more complex querying and filtering
  • Create a GitHub service class to encapsulate all API logic and make it more testable
  • Implement webhooks instead of scheduled commands to get real-time updates
  • Add retry logic and better error handling for API failures

But here's the thing: this is a personal website. The beauty of this implementation is its simplicity. It's easy to understand, maintain, and debug. There are no dependencies beyond Laravel's HTTP client, no database migrations to manage, and the entire feature lives in just two files.

For a personal site that updates once a day and displays a handful of contributions, this approach is perfect. It's maintainable, it works reliably, and I can understand exactly what it's doing six months from now when I inevitably need to make a small change.

The best part? Your visitors can see what you're actually building, not just what you claim to be building. That's powerful social proof for any developer's portfolio.

Resources

Happy coding! 🚀