October 18, 2025
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.

In terms of implementation, we'll be using:
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:
Plus, it's actually simpler to work with once you get the hang of it.
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 Command10{11 protected $signature = 'fetch:github-contributions';12 protected $description = 'Fetch recent GitHub contributions and stores them in cache';13 14 public function handle(): int15 {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(): array28 {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 structure48 $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 results55 return array_values(array_filter($nodes, fn ($node) => $node !== null));56 }57 58 protected function graphqlPullRequestQuery(): string59 {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 title66 url67 mergedAt68 bodyText69 additions70 deletions71 repository {72 name73 isPrivate74 owner {75 login76 }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],
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 months11query: "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.
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 Component13{14 public function render(): View|Closure|string15 {16 return view('components.recent-github-contributions', [17 'contributions' => $this->getContributions(),18 ]);19 }20 21 protected function getContributions(): Collection22 {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): Collection33 {34 return collect($contributions)35 ->map(fn ($contribution): array => $this->formatContribution($contribution));36 }37 38 private function formatContribution(mixed $contribution): array39 {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(): array66 {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}
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 <img11 src="{{ $contribution['avatar_url'] }}"12 alt="{{ $contribution['owner'] }}"13 class="w-10 h-10 rounded-full"14 >15 @endif16 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 @empty35 <p class="text-gray-400">No recent contributions found.</p>36 @endforelse37</div>
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>
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
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:
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.
Happy coding! 🚀