Build a CMS with Laravel and Vue - Part 2: Implementing posts

Introduction

In the previous part of this series, we set up user authentication and role authorization but we didn’t create any views for the application yet. In this section, we will create the Post model and start building the frontend for the application.

Our application allows different levels of accessibility for two kinds of users; the regular user and admin. In this chapter, we will focus on building the view that the regular users are permitted to see.

Before we build any views, let’s create the Post model as it is imperative to rendering the view.

The source code for this project is available here on GitHub.

Prerequisites

To follow along with this series, a few things are required:

  • Basic knowledge of PHP.
  • Basic knowledge of the Laravel framework.
  • Basic knowledge of JavaScript (ES6 syntax).
  • Basic knowledge of Vue.
  • Postman installed on your machine.

Setting up the Post model

We will create the Post model with an associated resource controller and a migration file using this command:

    $ php artisan make:model Post -mr

We added the r flag because we want the controller to be a resource controller. The m flag will generate a migration for the model.

Let’s navigate into the database/migrations folder and update the CreatePostsTable class that was generated for us:

1// File: ./app/database/migrations/*_create_posts_table.php
2    <?php 
3    
4    // [...]
5    
6    class CreatePostsTable extends Migration
7    {
8        public function up()
9        {
10            Schema::create('posts', function (Blueprint $table) {
11                $table->increments('id');
12                $table->integer('user_id')->unsigned();
13                $table->string('title');
14                $table->text('body');
15                $table->binary('image')->nullable();
16                $table->timestamps();
17            });
18        }
19    
20        public function down()
21        {
22            Schema::dropIfExists('posts');
23        }
24    }

We included a user_id property because we want to create a relationship between the User and Post models. A Post also has an image field, which is where its associated image’s address will be stored.

Creating a database seeder for the Post table

We will create a new seeder file for the posts table using this command:

    $ php artisan make:seeder PostTableSeeder

Let’s navigate into the database/seeds folder and update the PostTableSeeder.php file:

1// File: ./app/database/seeds/PostsTableSeeder.php
2    <?php 
3    
4    use App\Post;
5    use Illuminate\Database\Seeder;
6    
7    class PostTableSeeder extends Seeder
8    {
9        public function run()
10        {
11            $post = new Post;
12            $post->user_id = 2;
13            $post->title = "Using Laravel Seeders";
14            $post->body = "Laravel includes a simple method of seeding your database with test data using seed classes. All seed classes are stored in the database/seeds directory. Seed classes may have any name you wish, but probably should follow some sensible convention, such as UsersTableSeeder, etc. By default, a DatabaseSeeder class is defined for you. From this class, you may use the  call method to run other seed classes, allowing you to control the seeding order.";
15            $post->save();
16    
17            $post = new Post;
18            $post->user_id = 2;
19            $post->title = "Database: Migrations";
20            $post->body = "Migrations are like version control for your database, allowing your team to easily modify and share the application's database schema. Migrations are typically paired with Laravel's schema builder to easily build your application's database schema. If you have ever had to tell a teammate to manually add a column to their local database schema, you've faced the problem that database migrations solve.";
21            $post->save();
22        }
23    }

When we run this seeder, it will create two new posts and assign both of them to the admin user whose ID is 2. We are attaching both posts to the admin user because the regular users are only allowed to view posts and make comments; they can’t create a post.

Let’s open the DatabaseSeeder and update it with the following code:

1// File: ./app/database/seeds/DatabaseSeeder.php
2    <?php 
3    
4    use Illuminate\Database\Seeder;
5    
6    class DatabaseSeeder extends Seeder
7    {
8        public function run()
9        {
10            $this->call([
11                RoleTableSeeder::class,
12                UserTableSeeder::class,
13                PostTableSeeder::class,
14            ]);
15        }
16    }

We created the RoleTableSeeder and UserTableSeeder files in the previous chapter.

We will use this command to migrate our tables and seed the database:

    $ php artisan migrate:fresh --seed

Defining the relationships

Just as we previously created a many-to-many relationship between the User and Role models, we need to create a different kind of relationship between the Post and User models.

We will define the relationship as a one-to-many relationship because a user will have many posts but a post will only ever belong to one user.

Open the User model and include the method below:

1// File: ./app/User.php
2    public function posts()
3    {
4        return $this->hasMany(Post::class);
5    }

Open the Post model and include the method below:

1// File: ./app/Post.php
2    public function user()
3    {
4        return $this->belongsTo(User::class);
5    }

Setting up the routes

At this point in our application, we do not have a front page with all the posts listed. Let’s create so anyone can see all of the created posts. Asides from the front page, we also need a single post page in case a user needs to read a specific post.

Let’s include two new routes to our routes/web.php file:

  • The first route will match requests to the root of our application and will be handled by the PostController@all action:
    Route::get('/', 'PostController@all');

In the routes/web.php file, there will already be a route definition for the / address, you will have to replace it with the new route definition above.

  • The second route will handle requests for specific Post items and will be handled by the PostController@single action:
    Route::get('/posts/{post}', 'PostController@single');

With these two new routes added, here’s what the routes/web.php file should look like this:

1// File: ./routes/web.php
2    <?php 
3    
4    Auth::routes();
5    Route::get('/posts/{post}', 'PostController@single');
6    Route::get('/home', 'HomeController@index')->name('home');
7    Route::get('/', 'PostController@all');

Setting up the Post controller

In this section, we want to define the handler action methods that we registered in the routes/web.php file so that our application know how to render the matching views.

First, let’s add the all() method:

1// File: ./app/Http/Controllers/PostController.php
2    public function all()
3    {
4        return view('landing', [
5            'posts' => Post::latest()->paginate(5)
6        ]);
7    }

Here, we want to retrieve five created posts per page and send to the landing view. We will create this view shortly.

Next, let’s add the single() method to the controller:

1// File: ./app/Http/Controllers/PostController.php
2    public function single(Post $post)
3    {
4        return view('single', compact('post'));
5    }

In the method above, we used a feature of Laravel named route model binding to map the URL parameter to a Post instance with the same ID. We are returning a single view, which we will create shortly. This will be the view for the single post page.

Building our views

Laravel uses a templating engine called Blade for its frontend. We will use Blade to build these parts of the frontend before switching to Vue in the next chapter.

Navigate to the resources/views folder and create two new Blade files:

  • landing.blade.php
  • single.blade.php

These are the files that will load the views for the landing page and single post page. Before we start writing any code in these files, we want to create a simple layout template that our page views can use as a base.

In the resources/views/layouts folder, create a Blade template file and call it master.blade.php. This is where we will define the inheritable template for our single and landing pages.

Open the master.blade.php file and update it with this code:

1<!-- File: ./resources/views/layouts/master.blade.php -->
2    <!DOCTYPE html>
3    <html lang="en">
4      <head>
5        <meta charset="utf-8">
6        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7        <meta name="description" content="">
8        <meta name="author" content="Neo Ighodaro">
9        <title>LaravelCMS</title>
10        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
11        <style> 
12        body {
13          padding-top: 54px;
14        }
15        @media (min-width: 992px) {
16          body {
17              padding-top: 56px;
18          }
19        }
20        </style>
21      </head>
22      <body>
23        <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
24          <div class="container">
25            <a class="navbar-brand" href="/">LaravelCMS</a>
26            <div class="collapse navbar-collapse" id="navbarResponsive">
27              <ul class="navbar-nav ml-auto">
28                 @if (Route::has('login'))
29                    @auth
30                    <li class="nav-item">
31                         <a class="nav-link" href="{{ url('/home') }}">Home</a>
32                    </li>
33                    <li class="nav-item">
34                      <a class="nav-link" href="{{ route('logout') }}"
35                                           onclick="event.preventDefault();
36                                                         document.getElementById('logout-form').submit();">
37                        Log out
38                      </a>
39                      <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
40                        @csrf
41                      </form>
42                     </li>
43                     @else
44                     <li class="nav-item">
45                         <a class="nav-link" href="{{ route('login') }}">Login</a>
46                    </li>
47                     <li class="nav-item">
48                         <a class="nav-link" href="{{ route('register') }}">Register</a>
49                     </li>
50                     @endauth
51                 @endif
52              </ul>
53            </div>
54          </div>
55        </nav>
56      
57        <div id="app">
58            @yield('content')
59        </div>
60    
61        <footer class="py-5 bg-dark">
62          <div class="container">
63            <p class="m-0 text-center text-white">Copyright &copy; LaravelCMS 2018</p>
64          </div>
65        </footer>
66      </body>
67    </html>

Now we can inherit this template in the landing.blade.php file, open it and update it with this code:

1{{-- File: ./resources/views/landing.blade.php --}}
2    @extends('layouts.master')
3    
4    @section('content')
5    <div class="container">
6      <div class="row align-items-center">
7        <div class="col-md-8 mx-auto">
8          <h1 class="my-4 text-center">Welcome to the Blog </h1>
9      
10          @foreach ($posts as $post)
11          <div class="card mb-4">
12            <img class="card-img-top" src=" {!! !empty($post->image) ? '/uploads/posts/' . $post->image :  'http://placehold.it/750x300' !!} " alt="Card image cap">
13            <div class="card-body">
14              <h2 class="card-title text-center">{{ $post->title }}</h2>
15              <p class="card-text"> {{ str_limit($post->body, $limit = 280, $end = '...') }} </p>
16              <a href="/posts/{{ $post->id }}" class="btn btn-primary">Read More &rarr;</a>
17            </div>
18            <div class="card-footer text-muted">
19              Posted {{ $post->created_at->diffForHumans() }} by
20              <a href="#">{{ $post->user->name }} </a>
21            </div>
22          </div>
23          @endforeach
24          
25        </div>
26      </div>
27    </div>
28    @endsection

Let’s do the same with the single.blade.php file, open it and update it with this code:

1{{-- File: ./resources/views/single.blade.php --}}
2    @extends('layouts.master')
3    
4    @section('content')
5    <div class="container">
6      <div class="row">
7        <div class="col-lg-10 mx-auto">
8          <h3 class="mt-4">{{ $post->title }} <span class="lead"> by <a href="#"> {{ $post->user->name }} </a></span> </h3>
9          <hr>
10          <p>Posted {{ $post->created_at->diffForHumans() }} </p>
11          <hr>
12          <img class="img-fluid rounded" src=" {!! !empty($post->image) ? '/uploads/posts/' . $post->image :  'http://placehold.it/750x300' !!} " alt="">
13          <hr>
14          <p class="lead">{{ $post->body }}</p>
15          <hr>
16          <div class="card my-4">
17            <h5 class="card-header">Leave a Comment:</h5>
18            <div class="card-body">
19              <form>
20                <div class="form-group">
21                  <textarea class="form-control" rows="3"></textarea>
22                </div>
23                <button type="submit" class="btn btn-primary">Submit</button>
24              </form>
25            </div>
26          </div>
27        </div>
28      </div>
29    </div>
30    @endsection

Testing the application

We can test the application to see that things work as we expect. When we serve the application, we expect to see a landing page and a single post page. We also expect to see two posts because that’s the number of posts we seeded into the database.

We will serve the application using this command:

    $ php artisan serve

We can visit this address to see the application:

laravel-vue-cms-demo-part-2

We have used simple placeholder images here because we haven’t built the admin dashboard that allows CRUD operations to be performed on posts.

In the coming chapters, we will add the ability for an admin to include a custom image when creating a new post.

Conclusion

In this chapter, we created the Post model and defined a relationship on it to the User model. We also built the landing page and single page.

In the next part of this series, we will develop the API that will be the medium for communication between the admin user and the post items.

The source code for this project is available here on Github.