Datadog collects and monitors your PHP app metrics and distributed traces in real-time with application performance monitoring. Decrease downtime and performance issues with Datadog APM by tracing requests across service boundaries and drilling into individual traces end-to-end with flame graphs. Start your 14-day trial for free today.

Selling digital products using Laravel part 9: Serving ads on GitHub

Original – by Freek Van der Herten – 7 minute read

Our company has created a lot of open-source packages. At the moment of writing, we have over 200 packages, which have been downloaded nearly 100 million times. Because we think the package users might be interested in our paid offerings as well, we've put a small ad in the readme of each repo. In this blogpost I'll explain how we manage these ads using Laravel Nova and S3.

This is how an ad looks like at the laravel-tail repo.


The ad is not put at the top of the readme because users will want to see what they can do with a particular package before seeing an ad.

If a particular ad on a repo should be displayed until the end of time, the ad's image could be committed in the repo itself. But those ads should be rotated. Every week or month, we want to display another one. When launching a new product, it would be nice to let all repos show the new product's ad.

This problem is solved pragmatically. Let's take a look at the markdown of the "Support us" section of laravel-tail.

## Support us

[<img src="" width="419px" />](

You can see here that the image itself is not hosted on GitHub, but on an S3 bucket. Whenever we want to update the image on the repo, we don't change the repo's readme, but we update the image on the S3 bucket. Doing it this way also keeps our commit history clean. There's no need for a commit that updates the local at.

Also, note that you'll get redirected to when you click on the image. The Laravel app at will redirect the click to a page that matches the image being display. So it will redirect to,, and so on.

Administering ads and repositories in Nova

Let's take a look at how all of this is implemented on our site. We use Laravel Nova to administer ads. In the Ads module, we can upload an ad's image and specify to which URL clicks should be redirected.


In the Repositories module, we can choose which ad should be displayed on a particular repo.


Syncing ad images to S3

In the codebase, there is an action called SyncRepositoryAdImageToGitHubAdsDiskAction that will copy the image of an ad to the S3 bucket whenever an Ad or Repository is changed. Let's take a look at the action first.

namespace App\Actions;

use App\Models\Repository;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;

class SyncRepositoryAdImageToGitHubAdsDiskAction
    public Filesystem $disk;

    public function __construct()
        $this->disk = Storage::disk('github_ads');

    public function execute(Repository $repository): void
            ? $this->updateAdForRepository($repository)
            : $this->removeAdForRepository($repository);

    protected function updateAdForRepository(Repository $repository): void


    protected function removeAdForRepository(Repository $repository): void

You've probably noticed that all ads are saved on a disk named github_ads. Here's the definition of that disk in the filesystems.php config file.

'disks' => [

		// ...

    'github_ads' => [
        'driver' => env('GITHUB_ADS_DISK_DRIVER'),
        'root' => env('GITHUB_ADS_DISK_ROOT') ? storage_path(env('GITHUB_ADS_DISK_ROOT')) : '',
        'key' => env('GITHUB_ADS_DISK_KEY'),
        'secret' => env('GITHUB_ADS_DISK_SECRET'),
        'region' => env('GITHUB_ADS_DISK_REGION'),
        'bucket' => env('GITHUB_ADS_DISK_BUCKET'),
        'url' => env('GITHUB_ADS_DISK_URL'),
        'options' => [
            'CacheControl' => 'max-age=120, s-max-age=120',

In a local environment, the GITHUB_ADS_DISK_DRIVER is set to local. On production, that driver is set to s3, so an s3 bucket is used. Here's the content of that s3 bucket.


To make each file publicly available, we defined this policy on the bucket.


You probably also have noticed that we've set CacheControl on the disk definition, with a value of max-age=120, s-max-age=120. This will force S3 to add a Cache-Control header on all responses from the bucket.


This header needs to be present because otherwise, Camo (GitHub's image caching system) will indefinitely cache the image. We want to prevent that because otherwise, a change to an image would not be displayed.

I've already mentioned that SyncRepositoryAdImageToGitHubAdsDiskAction is executed whenever we change an Ad or Repository. Here's the relevant code in the Ad model.

public static function booted(): void
    self::saved(function (Ad $ad): void {
        if (in_array('image', $ad->getChanges())) {
            $ad->repositories->each(function (Repository $repository) {

    self::deleting(function (Ad $ad): void {
        $ad->repositories->each(function (Repository $repository) {

And here's where SyncRepositoryAdImageToGitHubAdsDiskAction is called in the Repository model.

public static function booted()
    self::saved(function (Repository $repository) {


Randomizing ads

Each week we want to randomize the ad displayed on a repo. Here's the job that randomizes the ads.

use App\Models\Ad;
use App\Models\Repository;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class RandomizeAdsOnGitHubRepositoriesJob implements ShouldQueue
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public function handle()
        $ads = Ad::active()->get();

        Repository::adShouldBeRandomized()->each(function (Repository $repository) use ($ads) {


That active scope just selects Ads of which the active attribute is set to true. When we launch a new product, we'll just make the ad for that new product the only active one, and dispatch RandomizeAdsOnGitHubRepositoriesJob. That will cause the ad for the new product to be displayed on all repos.

Redirecting ad clicks

In the markdown code snippet above, you saw that the user would go to this URL when the ad image gets clicked:

That URL is handled on our app by this route:

Route::get('github-ad-click/{repositoryName}', RedirectGitHubAdClickController::class)->name('github-ad-click');

Here's the code of RedirectGitHubAdClickController:

namespace App\Http\Controllers;

use App\Models\Repository;

class RedirectGitHubAdClickController
    public function __invoke(Repository $repository)
        if (! $ad = $repository->ad) {
            return redirect()->route('products.index');

        return redirect()->to($ad->click_redirect_url . "?utm_source=repo-{$repository->name}");

The code above is pretty straightforward. If the repository (in the example above laravel-tail) has a related Ad, we'll return a redirect to the URL defined on the ad. We'll tack on an utm_source. The source will allow us to track on which repo an ad was clicked.

If there's no ad, it'll return a redirect to the products page on

This series is continued in part 10: Miscellaneous interesting tidbits + outro.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.



Roman Pronskiy liked on 24th November 2020
/dev/faisal liked on 24th November 2020
Robert Cordes liked on 24th November 2020
Marijn Bent liked on 24th November 2020
Tauseef shah liked on 24th November 2020
Wouter Brouwers liked on 24th November 2020
. liked on 24th November 2020
@sammytongoi liked on 24th November 2020
Ken V. liked on 23rd November 2020
Jeroen van Rensen 🇳🇱 liked on 23rd November 2020
Niels liked on 23rd November 2020
tona.castelan liked on 23rd November 2020
Daniel Jones liked on 23rd November 2020
Richard Radermacher liked on 23rd November 2020
Bezhan Salleh liked on 23rd November 2020
Vikas Kapadiya liked on 23rd November 2020
Javier Pomachagua Pérez liked on 23rd November 2020
Kevin Purwito liked on 23rd November 2020
/dev/sam liked on 23rd November 2020
Günther Debrauwer liked on 23rd November 2020
Shivam Mathur liked on 23rd November 2020
Zubair Mohsin liked on 23rd November 2020
Sheng Slogar liked on 23rd November 2020
Roman Pronskiy liked on 16th October 2020
Robin Dirksen liked on 13th October 2020
Richard Radermacher liked on 13th October 2020