How to Create A Trending Articles List using Google Analytics with Laravel

In this article, I will guide you about building top trending articles using Google Analytics API with Laravel framework.

Recently I built this feature for my own blog, if you would like to view it open up the home page of my blog and see around the right sidebar.

I have used the popular laravel package laravel-analytics built by Spatie.

Prerequisites

Before starting to do the actual coding, first, you need to generate the API credentials required for API connectivity.

 

I am not going to cover the process of generating the API credentials, but I recommend you to follow the full steps explained by Spatie in their package documentation page.

Feature

The trending section I have built has following features, it was based on my own requirement.

If you would like to show the trending posts directly to your view without storing any records to the database or without any implicit logic, that is also provided by the package out of the box.

I will be doing it differently in this article, see below.

  • A console command to fetch the daily top views via Google Analytics API. Also, the command will purge the old records from database pulled before one week, that all depends on you, and you will be able to customize that all.
  • A helper class to fetch and manipulate the weekly page views pulled via API and group them by each blog.
  • The database process will be cached for every single day, so that will reduce queries to load the site faster.

Coding

Installing package via console mode with the composer.

composer require spatie/laravel-analytics

The package will be auto-registered in laravel v5.5 and above. If you are using lower version register the package by following the installation guide.

 

app/Blog.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * Class Blog
 * @package App
 */
class Blog extends Model
{
    /**
     * @var string
     */
    protected $table = 'blog';

    /**
     * @var array
     */
    protected $dates = ['published_at', 'deleted_at'];

    /**
     * @var string
     */
    protected $fillable = ['user_id', 'name', 'slug', 'excerpt', 'content', 'status', 'published_at'];

    /**
     * @param $slug
     */
    public static function findByTitle($slug = null)
    {
        return self::where('slug', $slug)->first();
    }
}

I am assuming that you are running blogs already, my blog model looks like above.

Adding new migration for trendings table.

php artisan make:migration create_trendings_table --create=trendings
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTrendingsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('trendings', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('blog_id')->nullable();
            $table->integer('views')->nullable();
            $table->string('url', 1024)->nullable();
            $table->string('page_title', 1024)->nullable();
            $table->tinyInteger('status')->default(1)->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('trendings');
    }
}

app/Trending.php

<?php

namespace App;

use App\Blog;
use Illuminate\Database\Eloquent\Model;
use Spatie\Analytics\Period;

/**
 * Class Trending
 * @package App
 */
class Trending extends Model
{
    /**
     * @var string
     */
    protected $table = 'trendings';

    /**
     * @var boolean
     */
    public $timestamps = true;

    /**
     * @var array
     */
    protected $dates = ['deleted_at'];

    /**
     * @var string
     */
    protected $fillable = ['blog_id', 'views', 'url', 'page_title'];

    /**
     * @return mixed
     */
    public function blog()
    {
        return $this->belongsTo(Blog::class);
    }

    /**
     * @param $query
     * @return mixed
     */
    public function scopeMonthly($query)
    {
        $period = Period::months(1);

        return $query->whereBetween('created_at', [
            $period->startDate,
            $period->endDate->endOfDay(),
        ]);
    }

    /**
     * @param $query
     * @return mixed
     */
    public function scopeWeekly($query)
    {
        $period = Period::days(7);

        return $query->whereBetween('created_at', [
            $period->startDate,
            $period->endDate->endOfDay(),
        ]);
    }
}

Adding new console command for API connectivity.

 

php artisan make:command TrendingCommand

<?php

namespace App\Console\Commands;

use App\Blog;
use App\Trending;
use Illuminate\Console\Command;
use Spatie\Analytics\AnalyticsFacade as Analytics;
use Spatie\Analytics\Period;

/**
 * Class TrendingCommand
 * @package App\Console\Commands
 */
class TrendingCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'analytics:trending';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Sync page view from Google Analytics API';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $pages = Analytics::fetchMostVisitedPages(Period::days(1), 300);

        if ($pages->count()) {
            $this->purge();

            $pages->map(function ($each) {
                $each = (object) $each;

                if (starts_with($each->url, '/blog/')) {
                    $slug = str_replace("/blog/", '', $each->url);
                    $blog = Blog::findByTitle($slug);

                    if (!empty($blog)) {
                        Trending::create([
                            'blog_id' => $blog->id,
                            'views' => $each->pageViews,
                            'status' => $blog->status,
                            'page_title' => $blog->name,
                            'url' => $each->url,
                        ]);

                        $this->info("{$blog->name} - {$each->pageViews} \n");
                    }
                }
            });
        }
    }

    /**
     * @return mixed
     */
    public function purge()
    {
        $period = Period::days(8);

        $this->info("Purging records before : {$period->startDate} \n");

        return Trending::where('created_at', '<', $period->startDate)
            ->forceDelete();
    }
}

Registering console command to run with the application.

app/Console/Kernel.php

    protected $commands = [
        'App\Console\Commands\TrendingCommand',
    ];

app/Helpers/Trending.php

<?php
namespace App\Helpers;

use App\Trending as Popular;
use Illuminate\Support\Facades\Cache;

/**
 * Class Trending
 * @package App\Helpers
 */
class Trending
{
    /**
     * @return mixed
     */
    public static function weekly($take = 15)
    {
        $collection = collect();

        $trendings = Cache::remember('popular', 60 * 24, function () {
            return Popular::with('blog')->weekly()->get();
        });

        $trendings->groupBy('blog_id')->map(function ($each) use ($collection) {
            $object = collect($each);

            $item = $object->first();
            $item->views = $object->sum('views');

            $collection->push($item);
        });

        return $collection->take($take);
    }
}

The job of the above helper class is to implicitly manipulate the seven days pages views and sort them out by the highest page views.

Now, creating new view partial to include it where ever you want to show the trending list.

resources/views/_trending.blade.php

@inject('trending', 'App\Helpers\Trending')

@php
    $trendings = $trending::weekly();
@endphp

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-header">Trending</div>
                    <div class="card-body">
                        @if($trendings->count())
                            <ul>
                                @foreach($trendings as $key => $each)
                                   <li>
                                        <div class="article">
                                            <a href="{{ asset($each->url) }}" title="{{ $each->views }}">
                                                <span class="text-muted">{{ $each->blog->published_at->format('d M, Y') }}</span><br>
                                                {{ $each->blog->name }}
                                            </a>
                                        </div>
                                    </li>
                                @endforeach
                            </ul>
                        @endif
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Alternative

If you would like to consume the real-time data via API instead of going through the above feature I built for my use case, you could just follow the package documentation and it has enough resources to understand.

 
// pull down real time data via Google Analytics API

Analytics::fetchMostVisitedPages(Period::days(7));

Now after completing the coding work, the most important thing is to set up a cron job that runs mid-night at 12:00 am to pull down the complete page views for that day.

My cron job looks like below.

0 0 * * * /usr/bin/php /home/deployer/project/artisan analytics:trending

Conclusion

Thanks for reading the article up to the end, if you are happy reading it and was helpful to you, feel free to share and leave your feedback below in the comment section. The article was written based on Laravel v5.6, feel free to utilize it with higher or lower versions as per your requirement. The full source code is available on github.

Happy Coding!