Laravel 5.1 Beauty - Adding Comments, RSS, and a Site Map

The final steps to finish the Laravel 5.1 Beauty Project

Posted on June 28, 2015 in L5 Beauty
Note: this is the final step in the tutorial.

Contents

The Problem with Comments

The main thing missing from the project right now is the ability for users to leave comments. Unfortunately, blog comments have multiple issues with them.

First of all there’s the moderation, approving, and general management of the comments. Yes, with Laravel 5.1 we could add this functionality to the blog’s administration, allowing users to sign up, allowing the users to make comments and so forth. It’d be easy enough to create management screens to handle all of this.

But the real problem with comments is SPAM.

How do we deal with that? CAPTCHA or reCAPTCHA? Blacklists and/or Whitelists? Creating a SPAM Honeypot trap like Maksim Surguy? Or by integrating Akismet?

Frankly, I don’t want the headaches. I want to use a proven, third-party solution that will keep my involvement to a minimum.

Adding Disqus Comments

We’ll use Disqus for comments on the Laravel 5.1 Beauty blog.

Creating a Disqus account

Head over to Disqus.com and sign up for a free Disqus account. Once you have an account, you’ll want to click on the link to Add Disqus to your site (If you can’t find the link, try clicking on the gear icon at the top right of the screen.)

You should see an image like the one below.

Figure 15.1 - Add Disqus to your site

Add Disqus to your site

After you fill out the simple form, choose Universal Code from the next screen. Then you’ll see a screen like the one below.

Figure 15.2 - Disqus Universal Code

Disqus Universal Code

Note the value of the variable disqus_shortname in this code.

Passing the Slug to the page

Next we’ll add the variable $slug to the pages showing posts. To do this modify the one line in the showPost() method of BlogController as instructed below.

Update to BlogController

// Change the line below
    return view($post->layout, compact('post', 'tag'));

// to this
    return view($post->layout, compact('post', 'tag', 'slug'));

Easy! Now the post view will have an additional variable.

Creating the Disqus Partial

Create a new file in the resources/views/blog/partials named disqus.blade.php with the content below.

Content of blog.partials.disqus view

@if (App::environment() === 'production')
  <div id="disqus_thread"></div>
  <script type="text/javascript">
    var disqus_shortname = 'SHORTNAME HERE';
    @if (isset($slug))
      var disqus_identifier = 'blog-{{ $slug }}';
    @endif

    (function() {
      var dsq = document.createElement('script');
      dsq.type = 'text/javascript';
      dsq.async = true;
      dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
      (document.getElementsByTagName('head')[0] ||
        document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
  </script>
  <noscript>
    Please enable JavaScript to view the
    <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a>
  </noscript>
  <a href="http://disqus.com" class="dsq-brlink">
    comments powered by <span class="logo-disqus">Disqus</span>
  </a>
@endif

Be sure an use the correct shortname noted earlier for SHORTNAME HERE in the above snippet.

If $slug is set then it sets the disqus identifier to group all the comments together for that page. $slug should now be set for all post pages.

You’ll also note that the whole template is wrapped in a big @if statement. This is because I didn’t want to show comments when I’m working on the blog locally. Feel free to change this to suit your needs.

Finally, update the page-footer.blade.php in the resources/views/blog/partials directory to match what’s below.

Updated blog.partials.page-footer

<hr>
<div class="container">
  <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
    @include('blog.partials.disqus')
  </div>
</div>
<hr>
<footer>
  <div class="container">
    <div class="row">
      <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        <p class="copyright">Copyright © {{ config('blog.author') }}</p>
      </div>
    </div>
  </div>
</footer>

Voila! Your blog now has comments.

Adding Social Links

Let’s make one, final, finishing touch in the footer and add links to your facebook account, twitter account, and so forth.

Edit page-footer.blade.php again, updating it to the final version below.

Final Version of blog.partials.page-footer view

<hr>
<div class="container">
  <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
    @include('blog.partials.disqus')
  </div>
</div>
<hr>
<footer>
  <div class="container">
    <div class="row">
      <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        <ul class="list-inline text-center">
          <li>
            <a href="{{ url('rss') }}" data-toggle="tooltip"
               title="RSS feed">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-rss fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
          <li>
            <a href="https://twitter.com/PERSONALIZE" data-toggle="tooltip"
               title="My Twitter Page">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-twitter fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
          <li>
            <a href="https://www.facebook.com/PERSONALIZE" data-toggle="tooltip"
               title="My Facebook Page">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-facebook fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
          <li>
            <a href="https://www.google.com/+PERSONALIZE" data-toggle="tooltip"
               title="My Google+ Page">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-google-plus fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
          <li>
            <a href="http://www.linkedin.com/in/PERSONALIZE/" data-toggle="tooltip"
               title="My LinkedIn Page">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-linkedin fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
          <li>
            <a href="https://github.com/PERSONALIZE" data-toggle="tooltip"
               title="My GitHub Pages">
              <span class="fa-stack fa-lg">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fa fa-github fa-stack-1x fa-inverse"></i>
              </span>
            </a>
          </li>
        </ul>
        <p class="copyright">Copyright © {{ config('blog.author') }}</p>
      </div>
    </div>
  </div>
</footer>

Be sure an update all those PERSONALIZE values with your own settings. If you don’t have a particular social account, just remove the entire item from the list.

Did you notice that first one? The RSS feed? That’s not yet created, so let’s do it next.

Creating a RSS Feed

A RSS feed is a must for any blogging application. Creating one in Laravel 5.1 for the L5Beauty application is quick and easy.

Pulling in the Composer Package

We’ll use the suin/php-rss-writer package to make creating RSS files easy.

The first step is to pull in the package with composer.

Requiring the suin/php-rss-writer Package with Composer

~/Code/l5beauty$ composer require suin/php-rss-writer
Using version ^1.3 for suin/php-rss-writer
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing suin/php-rss-writer (1.3)
    Downloading: 100%

Writing lock file
Generating autoload files
Generating optimized class loader

Creating the RSS Feed Service

Let’s create a service class to create and return the RSS feed. In the app/Services directory create the file RssFeed.php with the following content.

Content of RssFeed.php

<?php

namespace App\Services;

use App\Post;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Suin\RSSWriter\Channel;
use Suin\RSSWriter\Feed;
use Suin\RSSWriter\Item;

class RssFeed
{
  /**
   * Return the content of the RSS feed
   */
  public function getRSS()
  {
    if (Cache::has('rss-feed')) {
      return Cache::get('rss-feed');
    }

    $rss = $this->buildRssData();
    Cache::add('rss-feed', $rss, 120);

    return $rss;
  }

  /**
   * Return a string with the feed data
   *
   * @return string
   */
  protected function buildRssData()
  {
    $now = Carbon::now();
    $feed = new Feed();
    $channel = new Channel();
    $channel
      ->title(config('blog.title'))
      ->description(config('blog.description'))
      ->url(url())
      ->language('en')
      ->copyright('Copyright (c) '.config('blog.author'))
      ->lastBuildDate($now->timestamp)
      ->appendTo($feed);

    $posts = Post::where('published_at', '<=', $now)
      ->where('is_draft', 0)
      ->orderBy('published_at', 'desc')
      ->take(config('blog.rss_size'))
      ->get();
    foreach ($posts as $post) {
      $item = new Item();
      $item
        ->title($post->title)
        ->description($post->subtitle)
        ->url($post->url())
        ->pubDate($post->published_at->timestamp)
        ->guid($post->url(), true)
        ->appendTo($channel);
    }

    $feed = (string)$feed;

    // Replace a couple items to make the feed more compliant
    $feed = str_replace(
      '<rss version="2.0">',
      '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
      $feed
    );
    $feed = str_replace(
      '<channel>',
      '<channel>'."\n".'    <atom:link href="'.url('/rss').
      '" rel="self" type="application/rss+xml" />',
      $feed
    );

    return $feed;
  }
}

getRSS()
This method returns the entire feed as a string. We cache the results for 2 hours so the feed isn’t constantly being built.
buildRSSData()
This method build the feed itself from the posts table.

Updating the Blog Configuration

Add the rss_size entry to config/blog.php as shown below. We used this config value in the RSSFeed service class to determing how many posts to allow in the feed.

Content of config/blog.php

<?php
return [
    'name' => "L5 Beauty",
    'title' => "Laravel 5.1 Beauty",
    'subtitle' => 'A clean blog written in Laravel 5.1',
    'description' => 'This is my meta description',
    'author' => 'Chuck Heintzelman',
    'page_image' => 'home-bg.jpg',
    'posts_per_page' => 10,
    'rss_size' => 25,
    'contact_email' => env('MAIL_FROM'),
    'uploads' => [
        'storage' => 'local',
        'webpath' => '/uploads/',
    ],
];

Adding rss Route, Link, and Method

Only three things left to implement the RSS feed. First add a route in app/Http/routes.php as instructed below.

Adding rss Route

// After the following line
Route::post('contact', 'ContactController@sendContactInfo');

// Add the new route
get('rss', 'BlogController@rss');

Next, Update the blog.layouts.master view as instructed below.

Adding RSS Link to Web Pages

// Change the line below
  <title>{{ $title or config('blog.title') }}</title>

// To the following
  <title>{{ $title or config('blog.title') }}</title>

  <link rel="alternate" type="application/rss+xml" href="{{ url('rss') }}"
        title="RSS Feed {{ config('blog.title') }}">

Finally, update BlogController as instructed below.

BlogController updates for the RSS Feed

// Add the following use statement at the top of the file
use App\Services\RssFeed;

// Add the following method
  public function rss(RssFeed $feed)
  {
    $rss = $feed->getRSS();

    return response($rss)
      ->header('Content-type', 'application/rss+xml');
  }

That’s it. Point your browser to http://l5beauty.app/rss and you’ll see the feed.

Create a Site Map

To keep our blog search engine friendly we’ll add one final feature to the blog … a Site Map.

We’ll use the same technique as we did with the RSS Feed, to build it on-the-fly, but cache the results so it’s only rebuilt a maximum of once every couple hours.

Creating the SiteMap Service

Create a new file named SiteMap.php in the app/Services directory with the content below.

Content of SiteMap.php

<?php

namespace App\Services;

use App\Post;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;

class SiteMap
{
  /**
   * Return the content of the Site Map
   */
  public function getSiteMap()
  {
    if (Cache::has('site-map')) {
      return Cache::get('site-map');
    }

    $siteMap = $this->buildSiteMap();
    Cache::add('site-map', $siteMap, 120);
    return $siteMap;
  }

  /**
   * Build the Site Map
   */
  protected function buildSiteMap()
  {
    $postsInfo = $this->getPostsInfo();
    $dates = array_values($postsInfo);
    sort($dates);
    $lastmod = last($dates);
    $url = trim(url(), '/') . '/';

    $xml = [];
    $xml[] = '<?xml version="1.0" encoding="UTF-8"?'.'>';
    $xml[] = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
    $xml[] = '  <url>';
    $xml[] = "    <loc>$url</loc>";
    $xml[] = "    <lastmod>$lastmod</lastmod>";
    $xml[] = '    <changefreq>daily</changefreq>';
    $xml[] = '    <priority>0.8</priority>';
    $xml[] = '  </url>';

    foreach ($postsInfo as $slug => $lastmod) {
      $xml[] = '  <url>';
      $xml[] = "    <loc>{$url}blog/$slug</loc>";
      $xml[] = "    <lastmod>$lastmod</lastmod>";
      $xml[] = "  </url>";
    }

    $xml[] = '</urlset>';

    return join("\n", $xml);
  }

  /**
   * Return all the posts as $url => $date
   */
  protected function getPostsInfo()
  {
    return Post::where('published_at', '<=', Carbon::now())
      ->where('is_draft', 0)
      ->orderBy('published_at', 'desc')
      ->lists('updated_at', 'slug')
      ->all();
  }
}

Adding the sitemap.xml Route and Method

First edit your routes file as instructed below.

Addition of sitemap.xml route

// After this line
get('rss', 'BlogController@rss');

// Add the following line
get('sitemap.xml', 'BlogController@siteMap');

Then update the BlogController as instructed below.

Updates to BlogController for SiteMap

// Add the use below to the top of the file
use App\Services\SiteMap;

// Add the following method
  public function siteMap(SiteMap $siteMap)
  {
    $map = $siteMap->getSiteMap();

    return response($map)
      ->header('Content-type', 'text/xml');
  }

And point your browser to http://l5beauty.app/sitemap.xml to make sure it works.

It’s A Wrap!

That’s the end of the l5beauty project. You’ve developed a complete blogging system in Laravel 5.1 and hopefully have learned a lot along the way!

Recap

This chapter put all the final pieces together for the blog. We started using Disqus for comments and then created a RSS Feed for the project. Finally, the project was finished by adding a Site Map.

comments powered by Disqus