Build a news aggregator site with laravel

In this tutorial, We will learn how to create a web application that gathers top news across multiple news sites, using News API and Laravel.

News API is a simple and easy-to-use API that returns JSON metadata for the headlines currently published on a range of news sources and blogs.

At the end of this tutorial, you will be able to build this app.

newsapi_demo

First, visit Laravel to get started with setting up Laravel.

Our Checklist:

  1. Start a new laravel app.
  2. Install Guzzle via composer.
  3. Generate API keys on News API.
  4. Configure environment variables.
  5. Setting up a Helper method to make calls to the API.
  6. Create an Api Model. Here we will connect to the endpoints provided by News API.
  7. Connect to News API via guzzle.
  8. Create an ApiController.php file.
  9. Edit the route file.
  10. Edit welcome.blade.php file to handle our view.
  11. Create a CSS file.
  12. Create a Javascript file to handle our POST request.

Steps 1 and 2:

Run this command on the terminal

$ laravel new news-app

Navigate into the new app directory by running

$ cd news-app

Run this command on the terminal:

$ composer require guzzlehttp/guzzle

Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services. read about it here.

The first command laravel new news-app creates a new laravel app in your root folder while the second command adds Guzzle into your repo.

Tips: Run this command php artisan serve on your console to view your site, If you are on a mac, use Valet to serve your app( less hassle, less stress).

Steps 3 and 4:

Log on to News API and copy out your api key (P.S, You will need to create one).

Next, open the .env file and include the following environment variables


NEWS_API_KEY=_insert_news_api_key_here
NEWS_API_URL='https://newsapi.org/v2/'
DEFAULT_NEWS_SOURCE='CNN'
DEFAULT_NEWS_SOURCE_ID='cnn'

Remember to insert your newsapi key in place of _insert_news_api_key_here

In other to access the newly created environment variables, we need to include it in our app config file. Open the /config/app.php and edit as follows:


//File path: /config/app.php
[...]
    'url' => env('APP_URL', 'http://localhost'),
    [...]
    'news_api_url' => env('NEWS_API_URL', null),
    'news_api_key' => env('NEWS_API_KEY', null),
    'default_news_source' => env('DEFAULT_NEWS_SOURCE', 'CNN'),
    'default_news_source_id' => env('DEFAULT_NEWS_SOURCE_ID', 'cnn'),
 [...]

Question: Ehmm, but we do not want to be limited to cnn news alone, how do I do that?

Answer: Do not worry my dear friend, we will tweak this to our good from our codebase.


Step 5:

Since we will be making multiple calls to an external API using Guzzle, in other not to repeat ourselves on every call, we will create a helper class whose job will be to communicate with newsapi.org.

Run the following commands to create a new Service provider in the Providers directory:

$ php artisan make:provider HelperServiceProvider 

Once this is done, we need to register this Provider within our application configuration. We will do this by adding the new helper provider into the providers array of the app config file. Open the app config file and edit as follows:


//File path: /config/app.php

    'providers' => [
        
        [...]

        /*
         * Application Service Providers...
         */
        [...]
        App\Providers\RouteServiceProvider::class,
        App\Providers\HelperServiceProvider::class,

    ],

    [...]
    
    /*
    |--------------------------------------------------------------------------
    | Class Aliases
    |--------------------------------------------------------------------------
    |
    | This array of class aliases will be registered when this application
    | is started. However, feel free to register as many as you wish as
    | the aliases are "lazy" loaded so they don't hinder performance.
    |
    */

    'aliases' => [
        [...]
        'View' => Illuminate\Support\Facades\View::class,
        'Helper' => App\Helpers\Helper::class,
         
   ],

In other to have a cleaner architecture, we will create a Helper class in a new Helpers directory. This helper class can be used to store every other helper class.

Open the newly created file and the following:


//File path: /app/Providers/HelperServiceProvider.php

 public function register()
    {
        foreach (glob(app_path() . '/Helpers/*.php') as $file) {
            require_once($file);
        }
    }

Create the Helper folder and create a Helper file within the directory.


$ mkdir app/Helpers
$ touch app/Helper.php

Finally, open the Helper.php file and insert the following:


//File path: /app/Helpers/Helper.php

<?php

namespace App\Helpers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7;
class Helper
{
    /**
     * @param $url_params
     * @return mixed
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public function makeApiCalls($url_params)
    {
        try {
            $client = new Client();
            $apiRequest = $client->request('GET', config('app.news_api_url') .$url_params.'&apiKey=' . config('app.news_api_key'));
            return json_decode($apiRequest->getBody()->getContents(), true);
        } catch (RequestException $e) {
            //For handling exception
            echo Psr7\str($e->getRequest());
            if ($e->hasResponse()) {
                echo Psr7\str($e->getResponse());
            }
        }
    }
}

The makeApiCalls accepts a URL parameter, which in our case, will be the endpoint we aim to call, it makes the request using Guzzle, we get the response, decode it and send it back to the calling method.

Steps 6 and 7:

Next, we create an Api model.

Run this command on the terminal, Remember, you must be in your project directory to run this:

 
$ php artisan make:model Api

Open the newly created Api file located at news-app/app/Api.php. We will do two things:

  1. Create methods to fetch all available news from a selected news source.
  2. Create methods to fetch all available news sources.

//File path: /app/Api.php

<?php
namespace App;
use App\Helpers\Helper;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
/**
 * Class Api
 * @package App
 */
class Api extends Model
{
    /**
     * @param $newsSource
     * @return mixed
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public function fetchNewsFromSource($newsSource)
    {
        $urlParams = 'top-headlines?sources=' . $newsSource;
        $response = (new Helper)->makeApiCalls($urlParams);
        return Arr::get($response,'articles');
    }
    /**
     * @return mixed
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public function getAllSources()
    {
        $urlParams = 'sources?';
        $response = (new Helper)->makeApiCalls($urlParams);
        return Arr::get($response,'sources');
    }
}

Step 8:

Next, create an ApiController file. Run this command on the terminal


$ php artisan make:controller ApiController

Open the newly created ApiController.php file located at news-app/app/Http/Controllers/ApiController.php and here we will do three things:

  1. Create a newsapi function to handle POST and GET requests. Read about HTTP Methods here.
  2. Set CNN news as our default news source.
  3. Connect to our Api Method to retreive all news sources and selected source news.


//File path: /app/Http/Controllers/ApiController.php

<?php
namespace App\Http\Controllers;
use App\Api;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ApiController extends Controller
{
    /**
     * @param Request $request
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function displayNews(Request $request)
    {
        $response = $this->determineMethodHandler($request);
        $apiModel = new Api();
        $response['news'] = $apiModel->fetchNewsFromSource($response['sourceId']);
        $response['newsSources'] = $this->fetchAllNewsSources();
        return view('welcome', $response);
    }
    /**
     * @param $request
     * @return mixed
     */
    protected function determineMethodHandler($request)
    {
        if ($request->isMethod('get')) {
            $response['sourceName'] = config('app.default_news_source');
            $response['sourceId'] = config('app.default_news_source_id');
        } else {
            $request->validate([
                'source' => 'required|string',
            ]);
            $split_input = explode(':', $request->source);
            $response['sourceId'] = trim($split_input[0]);
            $response['sourceName'] = trim($split_input[1]);
        }
        return $response;
    }
    /**
     * @return mixed
     */
    public function fetchAllNewsSources()
    {
        $response = Cache::remember('allNewsSources', 22 * 60, function () {
            $api = new Api;
            return $api->getAllSources();
        });
        return $response;
    }
}

Step 9:

Edit the routes file: The routes file is located at news-app/routes/web.php. Here, we will do two things:

  1. Create a GET route that connects to the ApiController, this loads our default view and it will load news from cnn.
  2. Create a POST route that will pass the selected news source id to the ApiController. So when a user switches from cnn to Al-jazeera, this route will be used to pass the news source id.

<?php
//File path: /routes/web.php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', 'ApiController@displayNews');
Route::post('/sourceId', 'ApiController@displayNews');

Step 10:

Next, we navigate to our views directory and edit the contents of news-app/resources/views/welcome.blade.php with this:


<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
 <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1">
 
     <title>News Application with Laravel</title>
     <!-- Fonts -->
     <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" type="text/css">
 
     <!-- Styles -->
     <link href="{{ asset('css/app.css') }}" rel="stylesheet">
     <!-- Latest compiled and minified CSS -->
     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
 
 </head>
 <body>
 <div id="appendDivNews">
     <nav class="navbar fixed-top navbar-light bg-faded" style="background-color: #e3f2fd;">
         <a class="navbar-brand" href="#">News Around the World</a>
     </nav>
     {{ csrf_field() }}
     <section id="content" class="section-dropdown">
         <p class="select-header"> Select a news source: </p>
         <label class="select">
             <select name="news_sources" id="news_sources">
                 <option value="{{$sourceId}} : {{$sourceName}}">{{$sourceName}}</option>
                 @foreach ($newsSources as $newsSource)
                     <option value="{{$newsSource['id']}} : {{$newsSource['name'] }}">{{$newsSource['name']}}</option>
                 @endforeach
             </select>
 
         </label>
         <object id="spinner" data="spinner.svg" type="image/svg+xml" hidden></object>
     </section>
     <div id="news">
         <p> News Source : {{$sourceName}} </p>
 
 
         <section class="news">
             @foreach($news as $selectedNews)
 
                 <article>
                     <img src="{{$selectedNews['urlToImage']}}" alt=""/>
                     <div class="text">
                         <h1>{{$selectedNews['title']}}</h1>
                         <p style="font-size: 14px">{{$selectedNews['description']}} <a href="{{$selectedNews['url']}}"
                                                                                        target="_blank">
                                 <small>read more...</small>
                             </a></p>
                         <div style="padding-top: 5px;font-size: 12px">
                             Author: {{$selectedNews['author'] ? : "Unknown" }}</div>
                         @if($selectedNews['publishedAt'] !== null)
                             <div style="padding-top: 5px;">Date
                                 Published: {{ Carbon\Carbon::parse($selectedNews['publishedAt'])->format('l jS \\of F Y ') }}</div>
                         @else
                             <div style="padding-top: 5px;">Date Published: Unknown</div>
                         @endif
 
                     </div>
                 </article>
             @endforeach
         </section>
 
 
     </div>
 </div>
 
 </body>
 <!-- jQuery library -->
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
 
 <!-- Latest compiled JavaScript -->
 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
 <!-- Scripts -->
 <script src="{{ asset('js/site.js') }}"></script>
 
 </html>

Step 11:

For our css file, open news-app/public/css/app.css, You might choose to clear out the current contents. I minified the css file, you can unminify it here.


*,.select:after{box-sizing:border-box}*{font-family:Roboto,sans-serif}body{margin:0;background:#333;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}.news{padding:10px;text-align:center;font-size:0}.news article{display:inline-block;max-width:400px;margin:10px;background:#eee;text-align:left;vertical-align:top;font-size:1rem;box-shadow:0 0 40px -10px #000;overflow:hidden}.news article img{width:100%;height:200px;-o-object-fit:cover;object-fit:cover;-webkit-box-reflect:below 0 linear-gradient(0deg,rgba(0,0,0,.5),transparent 50%)}.news article .text{padding:20px;color:#333}.news article .text h1{margin:0 0 .5em;font-weight:500}.news article .text p{margin:0 0 1em}.news article .text p:last-child{margin:0}@media (max-width:600px){.news{padding:0}.news article{display:block;max-width:100%;margin:0 0 20px}.news article:last-child{margin:0}}.section-dropdown{width:300px;display:block;margin:50px auto;text-align:center}.select,select{height:40px;width:240px}.select{border:1px solid #cacaca;overflow:hidden;position:relative;display:block}#loading,.select:after{height:100%;text-align:center}select{padding:5px;border:0;font-size:16px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.select:after{content:"\f0dc";font-family:FontAwesome;color:#000;padding:12px 8px;position:absolute;right:0;top:0;background:#e3f2fd;z-index:1;width:10%;pointer-events:none}#loading{background-color:#fff;width:100%;position:fixed;z-index:9999}.select-header{text-align:left;padding-left:54px}

Step 12:

Finally, we edit our Javascript file. Navigate to news-app/public/js/app.js. You might choose to clear the contents of this file.


$('select').on('change', function() {
    let source = this.value;  //gets the selected news source from the news source dropdown menu
    let _token = $('input[name="_token"]').val();
    $("#spinner").show();
    $("#news").hide();
    $.ajax({
        type: "POST",
        url: "/sourceId",
        data: { source: source, _token : _token }, //posts the selected option to our ApiController file
        success:function(result){
            $("#spinner").hide();
            $("#news").show();
            // On success it gets `result`, which is a full html page that displays top news from the news source selected.
            $('#appendDivNews').html(result);    // Append the html result to the div that has an id  of  `appendDivNews`
        },

        error:function(){
            $("#spinner").hide();
            $("#news").show();
            alert("An error occurred, please try again!")
        }
    });

})

Conclusion

In other not to reload the page, Ajax was used to submit a POST request and the result received is a new page containing news details of the selected news source. The result was appended to the current view. With this, the page will not refresh.

A loader was used to show that an activity is happening, and as soon as the response is returned to the view, the loader disappears. You can download the loader here and include it in the public directory.

And that is it, we have a full-fledged News Application.

You can view/contribute to the codebase here.

View the code live here.

Site Footer